Run pylint from tox (#243)

This commit is contained in:
Pierre Ståhl
2020-12-17 09:26:55 +01:00
committed by GitHub
parent 3cf15f216e
commit f72cbc0b5a
9 changed files with 132 additions and 35 deletions

View File

@@ -209,7 +209,7 @@ async def async_setup(hass: HomeAssistant, config: dict):
await discovery.start() await discovery.start()
hass.data[DOMAIN][DATA_DISCOVERY] = discovery hass.data[DOMAIN][DATA_DISCOVERY] = discovery
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _shutdown) hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _shutdown)
except Exception: except Exception: # pylint: disable=broad-except
_LOGGER.exception("failed to set up discovery") _LOGGER.exception("failed to set up discovery")
hass.helpers.service.async_register_admin_service( hass.helpers.service.async_register_admin_service(

View File

@@ -65,7 +65,7 @@ async def async_setup_entry(
# Add DPS used by this platform to the request list # Add DPS used by this platform to the request list
for dp_conf in dps_config_fields: for dp_conf in dps_config_fields:
if dp_conf in device_config: 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( entities.append(
entity_class( entity_class(
@@ -93,11 +93,22 @@ def get_entity_config(config_entry, dp_id):
raise Exception(f"missing entity config for id {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): class TuyaDevice(pytuya.TuyaListener, pytuya.ContextualLogger):
"""Cache wrapper for pytuya.TuyaInterface.""" """Cache wrapper for pytuya.TuyaInterface."""
def __init__(self, hass, config_entry): def __init__(self, hass, config_entry):
"""Initialize the cache.""" """Initialize the cache."""
super().__init__()
self._hass = hass self._hass = hass
self._config_entry = config_entry self._config_entry = config_entry
self._interface = None self._interface = None
@@ -140,7 +151,7 @@ class TuyaDevice(pytuya.TuyaListener, pytuya.ContextualLogger):
raise Exception("Failed to retrieve status") raise Exception("Failed to retrieve status")
self.status_updated(status) self.status_updated(status)
except Exception: except Exception: # pylint: disable=broad-except
self.exception(f"Connect to {self._config_entry[CONF_HOST]} failed") self.exception(f"Connect to {self._config_entry[CONF_HOST]} failed")
if self._interface is not None: if self._interface is not None:
await self._interface.close() await self._interface.close()
@@ -161,7 +172,7 @@ class TuyaDevice(pytuya.TuyaListener, pytuya.ContextualLogger):
if self._interface is not None: if self._interface is not None:
try: try:
await self._interface.set_dp(state, dp_index) 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) self.exception("Failed to set DP %d to %d", dp_index, state)
else: else:
self.error( self.error(
@@ -173,7 +184,7 @@ class TuyaDevice(pytuya.TuyaListener, pytuya.ContextualLogger):
if self._interface is not None: if self._interface is not None:
try: try:
await self._interface.set_dps(states) await self._interface.set_dps(states)
except Exception: except Exception: # pylint: disable=broad-except
self.exception("Failed to set DPs %r", states) self.exception("Failed to set DPs %r", states)
else: else:
self.error( self.error(
@@ -203,6 +214,7 @@ class LocalTuyaEntity(Entity, pytuya.ContextualLogger):
def __init__(self, device, config_entry, dp_id, logger, **kwargs): def __init__(self, device, config_entry, dp_id, logger, **kwargs):
"""Initialize the Tuya entity.""" """Initialize the Tuya entity."""
super().__init__()
self._device = device self._device = device
self._config_entry = config_entry self._config_entry = config_entry
self._config = get_entity_config(config_entry, dp_id) self._config = get_entity_config(config_entry, dp_id)

View File

@@ -171,10 +171,10 @@ async def validate_input(hass: core.HomeAssistant, data):
) )
detected_dps = await interface.detect_available_dps() detected_dps = await interface.detect_available_dps()
except (ConnectionRefusedError, ConnectionResetError): except (ConnectionRefusedError, ConnectionResetError) as ex:
raise CannotConnect raise CannotConnect from ex
except ValueError: except ValueError as ex:
raise InvalidAuth raise InvalidAuth from ex
finally: finally:
if interface: if interface:
await interface.close() await interface.close()
@@ -230,7 +230,7 @@ class LocaltuyaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
errors["base"] = "address_in_use" errors["base"] = "address_in_use"
else: else:
errors["base"] = "discovery_failed" errors["base"] = "discovery_failed"
except Exception: except Exception: # pylint: disable= broad-except
_LOGGER.exception("discovery failed") _LOGGER.exception("discovery failed")
errors["base"] = "discovery_failed" errors["base"] = "discovery_failed"

View File

@@ -54,7 +54,7 @@ class TuyaDiscovery(asyncio.DatagramProtocol):
def close(self): def close(self):
"""Stop discovery.""" """Stop discovery."""
self.callback = None self._callback = None
for transport, _ in self._listeners: for transport, _ in self._listeners:
transport.close() transport.close()
@@ -63,7 +63,7 @@ class TuyaDiscovery(asyncio.DatagramProtocol):
data = data[20:-8] data = data[20:-8]
try: try:
data = decrypt_udp(data) data = decrypt_udp(data)
except Exception: except Exception: # pylint: disable=broad-except
data = data.decode() data = data.decode()
decoded = json.loads(data) decoded = json.loads(data)
@@ -81,10 +81,10 @@ class TuyaDiscovery(asyncio.DatagramProtocol):
async def discover(): async def discover():
"""Discover and return devices on local network.""" """Discover and return devices on local network."""
discover = TuyaDiscovery() discovery = TuyaDiscovery()
try: try:
await discover.start() await discovery.start()
await asyncio.sleep(DEFAULT_TIMEOUT) await asyncio.sleep(DEFAULT_TIMEOUT)
finally: finally:
discover.close() discovery.close()
return discover.devices return discovery.devices

View File

@@ -188,7 +188,7 @@ class LocaltuyaLight(LocalTuyaEntity, LightEntity):
"""Return the hs color value.""" """Return the hs color value."""
if self.is_color_mode: if self.is_color_mode:
return self._hs return self._hs
elif self.supported_features & SUPPORT_COLOR: if self.supported_features & SUPPORT_COLOR:
return [0, 0] return [0, 0]
return None return None

View File

@@ -178,7 +178,7 @@ class AESCipher:
def __init__(self, key): def __init__(self, key):
"""Initialize a new AESCipher.""" """Initialize a new AESCipher."""
self.bs = 16 self.block_size = 16
self.cipher = Cipher(algorithms.AES(key), modes.ECB(), default_backend()) self.cipher = Cipher(algorithms.AES(key), modes.ECB(), default_backend())
def encrypt(self, raw, use_base64=True): def encrypt(self, raw, use_base64=True):
@@ -195,13 +195,13 @@ class AESCipher:
decryptor = self.cipher.decryptor() decryptor = self.cipher.decryptor()
return self._unpad(decryptor.update(enc) + decryptor.finalize()).decode() return self._unpad(decryptor.update(enc) + decryptor.finalize()).decode()
def _pad(self, s): def _pad(self, data):
padnum = self.bs - len(s) % self.bs padnum = self.block_size - len(data) % self.block_size
return s + padnum * chr(padnum).encode() return data + padnum * chr(padnum).encode()
@staticmethod @staticmethod
def _unpad(s): def _unpad(data):
return s[: -ord(s[len(s) - 1 :])] return data[: -ord(data[len(data) - 1 :])]
class MessageDispatcher(ContextualLogger): class MessageDispatcher(ContextualLogger):
@@ -213,6 +213,7 @@ class MessageDispatcher(ContextualLogger):
def __init__(self, dev_id, listener): def __init__(self, dev_id, listener):
"""Initialize a new MessageBuffer.""" """Initialize a new MessageBuffer."""
super().__init__()
self.buffer = b"" self.buffer = b""
self.listeners = {} self.listeners = {}
self.listener = listener self.listener = listener
@@ -311,7 +312,7 @@ class TuyaListener(ABC):
"""Device updated status.""" """Device updated status."""
@abstractmethod @abstractmethod
def disconnected(self, exc): def disconnected(self):
"""Device disconnected.""" """Device disconnected."""
@@ -321,7 +322,7 @@ class EmptyListener(TuyaListener):
def status_updated(self, status): def status_updated(self, status):
"""Device updated status.""" """Device updated status."""
def disconnected(self, exc): def disconnected(self):
"""Device disconnected.""" """Device disconnected."""
@@ -340,6 +341,7 @@ class TuyaProtocol(asyncio.Protocol, ContextualLogger):
Attributes: Attributes:
port (int): The port to connect to. port (int): The port to connect to.
""" """
super().__init__()
self.loop = asyncio.get_running_loop() self.loop = asyncio.get_running_loop()
self.set_logger(_LOGGER, dev_id) self.set_logger(_LOGGER, dev_id)
self.id = dev_id self.id = dev_id
@@ -381,7 +383,7 @@ class TuyaProtocol(asyncio.Protocol, ContextualLogger):
except asyncio.CancelledError: except asyncio.CancelledError:
self.debug("Stopped heartbeat loop") self.debug("Stopped heartbeat loop")
raise raise
except Exception as ex: except Exception as ex: # pylint: disable=broad-except
self.exception("Heartbeat failed (%s), disconnecting", ex) self.exception("Heartbeat failed (%s), disconnecting", ex)
break break
@@ -404,7 +406,7 @@ class TuyaProtocol(asyncio.Protocol, ContextualLogger):
listener = self.listener and self.listener() listener = self.listener and self.listener()
if listener is not None: if listener is not None:
listener.disconnected() listener.disconnected()
except Exception: except Exception: # pylint: disable=broad-except
self.exception("Failed to call disconnected callback") self.exception("Failed to call disconnected callback")
async def close(self): async def close(self):
@@ -503,8 +505,8 @@ class TuyaProtocol(asyncio.Protocol, ContextualLogger):
self.add_dps_to_request(range(*dps_range)) self.add_dps_to_request(range(*dps_range))
try: try:
data = await self.status() data = await self.status()
except Exception as e: except Exception as ex:
self.exception("Failed to get status: %s", e) self.exception("Failed to get status: %s", ex)
raise raise
if "dps" in data: if "dps" in data:
self.dps_cache.update(data["dps"]) self.dps_cache.update(data["dps"])
@@ -525,7 +527,7 @@ class TuyaProtocol(asyncio.Protocol, ContextualLogger):
if not payload: if not payload:
payload = "{}" payload = "{}"
elif payload.startswith(b"{"): elif payload.startswith(b"{"):
payload = payload pass
elif payload.startswith(PROTOCOL_VERSION_BYTES_31): elif payload.startswith(PROTOCOL_VERSION_BYTES_31):
payload = payload[len(PROTOCOL_VERSION_BYTES_31) :] # remove version header payload = payload[len(PROTOCOL_VERSION_BYTES_31) :] # remove version header
# remove (what I'm guessing, but not confirmed is) 16-bytes of MD5 # 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 payload = PROTOCOL_33_HEADER + payload
elif command == SET: elif command == SET:
payload = self.cipher.encrypt(payload) payload = self.cipher.encrypt(payload)
preMd5String = ( to_hash = (
b"data=" b"data="
+ payload + payload
+ b"||lpv=" + b"||lpv="
@@ -599,9 +601,9 @@ class TuyaProtocol(asyncio.Protocol, ContextualLogger):
+ b"||" + b"||"
+ self.local_key + self.local_key
) )
m = md5() hasher = md5()
m.update(preMd5String) hasher.update(to_hash)
hexdigest = m.hexdigest() hexdigest = hasher.hexdigest()
payload = ( payload = (
PROTOCOL_VERSION_BYTES_31 PROTOCOL_VERSION_BYTES_31
+ hexdigest[8:][:16].encode("latin1") + hexdigest[8:][:16].encode("latin1")

View File

@@ -1,3 +1,82 @@
[tool.black] [tool.black]
target-version = ["py37", "py38"] target-version = ["py37", "py38"]
include = 'custom_components/localtuya/.*\.py' 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"

View File

@@ -3,4 +3,7 @@ codespell==1.17.1
flake8==3.8.3 flake8==3.8.3
mypy==0.782 mypy==0.782
pydocstyle==5.1.1 pydocstyle==5.1.1
cryptography==2.9.2 cryptography==3.2
pylint==2.6.0
pylint-strict-informational==0.1
homeassistant==2020.12.0

View File

@@ -31,6 +31,7 @@ commands =
flake8 custom_components flake8 custom_components
black --fast --check . black --fast --check .
pydocstyle -v custom_components pydocstyle -v custom_components
pylint custom_components/localtuya
[testenv:typing] [testenv:typing]
commands = commands =