From f72cbc0b5a39858ddb7da003081daed4e0ce6c33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20St=C3=A5hl?= Date: Thu, 17 Dec 2020 09:26:55 +0100 Subject: [PATCH] Run pylint from tox (#243) --- custom_components/localtuya/__init__.py | 2 +- custom_components/localtuya/common.py | 20 ++++- custom_components/localtuya/config_flow.py | 10 +-- custom_components/localtuya/discovery.py | 12 +-- custom_components/localtuya/light.py | 2 +- .../localtuya/pytuya/__init__.py | 36 +++++---- pyproject.toml | 79 +++++++++++++++++++ requirements_test.txt | 5 +- tox.ini | 1 + 9 files changed, 132 insertions(+), 35 deletions(-) diff --git a/custom_components/localtuya/__init__.py b/custom_components/localtuya/__init__.py index d575198..ec30493 100644 --- a/custom_components/localtuya/__init__.py +++ b/custom_components/localtuya/__init__.py @@ -209,7 +209,7 @@ async def async_setup(hass: HomeAssistant, config: dict): await discovery.start() hass.data[DOMAIN][DATA_DISCOVERY] = discovery hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _shutdown) - except Exception: + except Exception: # pylint: disable=broad-except _LOGGER.exception("failed to set up discovery") hass.helpers.service.async_register_admin_service( diff --git a/custom_components/localtuya/common.py b/custom_components/localtuya/common.py index f8b84f1..05bd49c 100644 --- a/custom_components/localtuya/common.py +++ b/custom_components/localtuya/common.py @@ -65,7 +65,7 @@ async def async_setup_entry( # Add DPS used by this platform to the request list for dp_conf in dps_config_fields: if dp_conf in device_config: - tuyainterface._dps_to_request[device_config[dp_conf]] = None + tuyainterface.add_dps_to_request(device_config[dp_conf]) entities.append( entity_class( @@ -93,11 +93,22 @@ def get_entity_config(config_entry, dp_id): raise Exception(f"missing entity config for id {dp_id}") +@callback +def async_config_entry_by_device_id(hass, device_id): + """Look up config entry by device id.""" + current_entries = hass.config_entries.async_entries(DOMAIN) + for entry in current_entries: + if entry.data[CONF_DEVICE_ID] == device_id: + return entry + return None + + class TuyaDevice(pytuya.TuyaListener, pytuya.ContextualLogger): """Cache wrapper for pytuya.TuyaInterface.""" def __init__(self, hass, config_entry): """Initialize the cache.""" + super().__init__() self._hass = hass self._config_entry = config_entry self._interface = None @@ -140,7 +151,7 @@ class TuyaDevice(pytuya.TuyaListener, pytuya.ContextualLogger): raise Exception("Failed to retrieve status") self.status_updated(status) - except Exception: + except Exception: # pylint: disable=broad-except self.exception(f"Connect to {self._config_entry[CONF_HOST]} failed") if self._interface is not None: await self._interface.close() @@ -161,7 +172,7 @@ class TuyaDevice(pytuya.TuyaListener, pytuya.ContextualLogger): if self._interface is not None: try: await self._interface.set_dp(state, dp_index) - except Exception: + except Exception: # pylint: disable=broad-except self.exception("Failed to set DP %d to %d", dp_index, state) else: self.error( @@ -173,7 +184,7 @@ class TuyaDevice(pytuya.TuyaListener, pytuya.ContextualLogger): if self._interface is not None: try: await self._interface.set_dps(states) - except Exception: + except Exception: # pylint: disable=broad-except self.exception("Failed to set DPs %r", states) else: self.error( @@ -203,6 +214,7 @@ class LocalTuyaEntity(Entity, pytuya.ContextualLogger): def __init__(self, device, config_entry, dp_id, logger, **kwargs): """Initialize the Tuya entity.""" + super().__init__() self._device = device self._config_entry = config_entry self._config = get_entity_config(config_entry, dp_id) diff --git a/custom_components/localtuya/config_flow.py b/custom_components/localtuya/config_flow.py index 23ac46b..d2f4b98 100644 --- a/custom_components/localtuya/config_flow.py +++ b/custom_components/localtuya/config_flow.py @@ -171,10 +171,10 @@ async def validate_input(hass: core.HomeAssistant, data): ) detected_dps = await interface.detect_available_dps() - except (ConnectionRefusedError, ConnectionResetError): - raise CannotConnect - except ValueError: - raise InvalidAuth + except (ConnectionRefusedError, ConnectionResetError) as ex: + raise CannotConnect from ex + except ValueError as ex: + raise InvalidAuth from ex finally: if interface: await interface.close() @@ -230,7 +230,7 @@ class LocaltuyaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors["base"] = "address_in_use" else: errors["base"] = "discovery_failed" - except Exception: + except Exception: # pylint: disable= broad-except _LOGGER.exception("discovery failed") errors["base"] = "discovery_failed" diff --git a/custom_components/localtuya/discovery.py b/custom_components/localtuya/discovery.py index 5dbe285..c7f35c3 100644 --- a/custom_components/localtuya/discovery.py +++ b/custom_components/localtuya/discovery.py @@ -54,7 +54,7 @@ class TuyaDiscovery(asyncio.DatagramProtocol): def close(self): """Stop discovery.""" - self.callback = None + self._callback = None for transport, _ in self._listeners: transport.close() @@ -63,7 +63,7 @@ class TuyaDiscovery(asyncio.DatagramProtocol): data = data[20:-8] try: data = decrypt_udp(data) - except Exception: + except Exception: # pylint: disable=broad-except data = data.decode() decoded = json.loads(data) @@ -81,10 +81,10 @@ class TuyaDiscovery(asyncio.DatagramProtocol): async def discover(): """Discover and return devices on local network.""" - discover = TuyaDiscovery() + discovery = TuyaDiscovery() try: - await discover.start() + await discovery.start() await asyncio.sleep(DEFAULT_TIMEOUT) finally: - discover.close() - return discover.devices + discovery.close() + return discovery.devices diff --git a/custom_components/localtuya/light.py b/custom_components/localtuya/light.py index b419909..b7745e7 100644 --- a/custom_components/localtuya/light.py +++ b/custom_components/localtuya/light.py @@ -188,7 +188,7 @@ class LocaltuyaLight(LocalTuyaEntity, LightEntity): """Return the hs color value.""" if self.is_color_mode: return self._hs - elif self.supported_features & SUPPORT_COLOR: + if self.supported_features & SUPPORT_COLOR: return [0, 0] return None diff --git a/custom_components/localtuya/pytuya/__init__.py b/custom_components/localtuya/pytuya/__init__.py index 2e9670b..59fd929 100644 --- a/custom_components/localtuya/pytuya/__init__.py +++ b/custom_components/localtuya/pytuya/__init__.py @@ -178,7 +178,7 @@ class AESCipher: def __init__(self, key): """Initialize a new AESCipher.""" - self.bs = 16 + self.block_size = 16 self.cipher = Cipher(algorithms.AES(key), modes.ECB(), default_backend()) def encrypt(self, raw, use_base64=True): @@ -195,13 +195,13 @@ class AESCipher: decryptor = self.cipher.decryptor() return self._unpad(decryptor.update(enc) + decryptor.finalize()).decode() - def _pad(self, s): - padnum = self.bs - len(s) % self.bs - return s + padnum * chr(padnum).encode() + def _pad(self, data): + padnum = self.block_size - len(data) % self.block_size + return data + padnum * chr(padnum).encode() @staticmethod - def _unpad(s): - return s[: -ord(s[len(s) - 1 :])] + def _unpad(data): + return data[: -ord(data[len(data) - 1 :])] class MessageDispatcher(ContextualLogger): @@ -213,6 +213,7 @@ class MessageDispatcher(ContextualLogger): def __init__(self, dev_id, listener): """Initialize a new MessageBuffer.""" + super().__init__() self.buffer = b"" self.listeners = {} self.listener = listener @@ -311,7 +312,7 @@ class TuyaListener(ABC): """Device updated status.""" @abstractmethod - def disconnected(self, exc): + def disconnected(self): """Device disconnected.""" @@ -321,7 +322,7 @@ class EmptyListener(TuyaListener): def status_updated(self, status): """Device updated status.""" - def disconnected(self, exc): + def disconnected(self): """Device disconnected.""" @@ -340,6 +341,7 @@ class TuyaProtocol(asyncio.Protocol, ContextualLogger): Attributes: port (int): The port to connect to. """ + super().__init__() self.loop = asyncio.get_running_loop() self.set_logger(_LOGGER, dev_id) self.id = dev_id @@ -381,7 +383,7 @@ class TuyaProtocol(asyncio.Protocol, ContextualLogger): except asyncio.CancelledError: self.debug("Stopped heartbeat loop") raise - except Exception as ex: + except Exception as ex: # pylint: disable=broad-except self.exception("Heartbeat failed (%s), disconnecting", ex) break @@ -404,7 +406,7 @@ class TuyaProtocol(asyncio.Protocol, ContextualLogger): listener = self.listener and self.listener() if listener is not None: listener.disconnected() - except Exception: + except Exception: # pylint: disable=broad-except self.exception("Failed to call disconnected callback") async def close(self): @@ -503,8 +505,8 @@ class TuyaProtocol(asyncio.Protocol, ContextualLogger): self.add_dps_to_request(range(*dps_range)) try: data = await self.status() - except Exception as e: - self.exception("Failed to get status: %s", e) + except Exception as ex: + self.exception("Failed to get status: %s", ex) raise if "dps" in data: self.dps_cache.update(data["dps"]) @@ -525,7 +527,7 @@ class TuyaProtocol(asyncio.Protocol, ContextualLogger): if not payload: payload = "{}" elif payload.startswith(b"{"): - payload = payload + pass elif payload.startswith(PROTOCOL_VERSION_BYTES_31): payload = payload[len(PROTOCOL_VERSION_BYTES_31) :] # remove version header # remove (what I'm guessing, but not confirmed is) 16-bytes of MD5 @@ -591,7 +593,7 @@ class TuyaProtocol(asyncio.Protocol, ContextualLogger): payload = PROTOCOL_33_HEADER + payload elif command == SET: payload = self.cipher.encrypt(payload) - preMd5String = ( + to_hash = ( b"data=" + payload + b"||lpv=" @@ -599,9 +601,9 @@ class TuyaProtocol(asyncio.Protocol, ContextualLogger): + b"||" + self.local_key ) - m = md5() - m.update(preMd5String) - hexdigest = m.hexdigest() + hasher = md5() + hasher.update(to_hash) + hexdigest = hasher.hexdigest() payload = ( PROTOCOL_VERSION_BYTES_31 + hexdigest[8:][:16].encode("latin1") diff --git a/pyproject.toml b/pyproject.toml index 6af5e6d..d90fe71 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,82 @@ [tool.black] target-version = ["py37", "py38"] include = 'custom_components/localtuya/.*\.py' + +# pylint config stolen from Home Assistant +# Use a conservative default here; 2 should speed up most setups and not hurt +# any too bad. Override on command line as appropriate. +# Disabled for now: https://github.com/PyCQA/pylint/issues/3584 +#jobs = 2 +load-plugins = [ + "pylint_strict_informational", +] +persistent = false +extension-pkg-whitelist = [ + "ciso8601", + "cv2", +] + +[tool.pylint.BASIC] +good-names = [ + "_", + "ev", + "ex", + "fp", + "i", + "id", + "j", + "k", + "Run", + "T", + "hs", +] + +[tool.pylint."MESSAGES CONTROL"] +# Reasons disabled: +# format - handled by black +# locally-disabled - it spams too much +# duplicate-code - unavoidable +# cyclic-import - doesn't test if both import on load +# abstract-class-little-used - prevents from setting right foundation +# unused-argument - generic callbacks and setup methods create a lot of warnings +# too-many-* - are not enforced for the sake of readability +# too-few-* - same as too-many-* +# abstract-method - with intro of async there are always methods missing +# inconsistent-return-statements - doesn't handle raise +# too-many-ancestors - it's too strict. +# wrong-import-order - isort guards this +disable = [ + "format", + "abstract-class-little-used", + "abstract-method", + "cyclic-import", + "duplicate-code", + "inconsistent-return-statements", + "locally-disabled", + "not-context-manager", + "too-few-public-methods", + "too-many-ancestors", + "too-many-arguments", + "too-many-branches", + "too-many-instance-attributes", + "too-many-lines", + "too-many-locals", + "too-many-public-methods", + "too-many-return-statements", + "too-many-statements", + "too-many-boolean-expressions", + "unused-argument", + "wrong-import-order", +] +enable = [ + "use-symbolic-message-instead", +] + +[tool.pylint.REPORTS] +score = false + +[tool.pylint.FORMAT] +expected-line-ending-format = "LF" + +[tool.pylint.MISCELLANEOUS] +notes = "XXX" \ No newline at end of file diff --git a/requirements_test.txt b/requirements_test.txt index 8a7dd4e..0729f96 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -3,4 +3,7 @@ codespell==1.17.1 flake8==3.8.3 mypy==0.782 pydocstyle==5.1.1 -cryptography==2.9.2 \ No newline at end of file +cryptography==3.2 +pylint==2.6.0 +pylint-strict-informational==0.1 +homeassistant==2020.12.0 diff --git a/tox.ini b/tox.ini index e311a19..49ccccb 100644 --- a/tox.ini +++ b/tox.ini @@ -31,6 +31,7 @@ commands = flake8 custom_components black --fast --check . pydocstyle -v custom_components + pylint custom_components/localtuya [testenv:typing] commands =