From 056c72d50d6274a528f736345691e9c51de67b63 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Sat, 7 Dec 2019 18:28:55 +0100 Subject: [PATCH] Drop Python 2 Support (#793) * Remove Python 2 support * Remove u-strings * Remove docker symlinks * Remove from travis * Update requirements * Upgrade flake8/pylint * Fixes * Manual * Run pyupgrade * Lint * Remove base_int * Fix * Update platformio_api.py * Update component.cpp --- .travis.yml | 6 - docker/Dockerfile.lint | 2 - esphome/__main__.py | 61 +++--- esphome/api/client.py | 45 ++-- esphome/automation.py | 4 +- esphome/components/ade7953/sensor.py | 2 +- esphome/components/ads1115/sensor.py | 7 +- esphome/components/api/__init__.py | 2 +- esphome/components/binary_sensor/__init__.py | 17 +- esphome/components/deep_sleep/__init__.py | 4 +- esphome/components/display/__init__.py | 3 +- .../components/esp32_ble_tracker/__init__.py | 12 +- .../components/esp32_touch/binary_sensor.py | 2 +- esphome/components/font/__init__.py | 20 +- esphome/components/globals/__init__.py | 3 +- esphome/components/hmc5883l/sensor.py | 6 +- esphome/components/http_request/__init__.py | 8 +- esphome/components/image/__init__.py | 3 +- esphome/components/ina219/sensor.py | 1 - esphome/components/ina226/sensor.py | 1 - esphome/components/ina3221/sensor.py | 1 - esphome/components/light/effects.py | 4 +- esphome/components/logger/__init__.py | 19 +- esphome/components/mpu6050/sensor.py | 8 +- esphome/components/mqtt/__init__.py | 10 +- esphome/components/neopixelbus/light.py | 2 +- esphome/components/ntc/sensor.py | 3 +- esphome/components/partition/light.py | 4 +- esphome/components/pmsx003/sensor.py | 2 +- esphome/components/qmc5883l/sensor.py | 6 +- esphome/components/remote_base/__init__.py | 25 ++- .../components/remote_transmitter/switch.py | 6 +- esphome/components/stepper/__init__.py | 4 +- esphome/components/substitutions/__init__.py | 19 +- esphome/components/sun/__init__.py | 3 +- esphome/components/tcs34725/sensor.py | 1 - esphome/components/time/__init__.py | 42 ++-- esphome/components/uart/__init__.py | 7 +- esphome/components/uart/switch/__init__.py | 5 +- esphome/components/wifi/__init__.py | 4 +- esphome/config.py | 172 +++++++-------- esphome/config_helpers.py | 7 +- esphome/config_validation.py | 202 ++++++++--------- esphome/const.py | 25 ++- esphome/core.py | 129 ++++++----- esphome/core/component.cpp | 4 + esphome/core_config.py | 12 +- esphome/cpp_generator.py | 203 +++++++++--------- esphome/cpp_helpers.py | 15 +- esphome/dashboard/dashboard.py | 49 ++--- esphome/espota2.py | 51 ++--- esphome/helpers.py | 54 ++--- esphome/legacy.py | 1 - esphome/mqtt.py | 46 ++-- esphome/pins.py | 30 ++- esphome/platformio_api.py | 11 +- esphome/py_compat.py | 89 -------- esphome/storage_json.py | 16 +- esphome/util.py | 59 ++--- esphome/voluptuous_schema.py | 18 +- esphome/vscode.py | 14 +- esphome/wizard.py | 73 +++---- esphome/writer.py | 76 ++++--- esphome/yaml_util.py | 76 +++---- esphome/zeroconf.py | 32 ++- pylintrc | 6 - requirements.txt | 1 - requirements_test.txt | 1 - script/api_protobuf/api_options_pb2.py | 1 - script/api_protobuf/api_protobuf.py | 8 +- script/build_compile_commands.py | 2 +- script/ci-custom.py | 17 +- script/clang-format | 2 +- script/clang-tidy | 2 +- script/helpers.py | 12 +- script/lint-python | 6 +- setup.cfg | 1 - setup.py | 5 +- 78 files changed, 815 insertions(+), 1097 deletions(-) diff --git a/.travis.yml b/.travis.yml index c7f75a2cb9..ca0a3082db 100644 --- a/.travis.yml +++ b/.travis.yml @@ -21,12 +21,6 @@ matrix: - esphome tests/test1.yaml compile - esphome tests/test2.yaml compile - esphome tests/test3.yaml compile - - python: "2.7" - env: TARGET=Test2.7 - script: - - esphome tests/test1.yaml compile - - esphome tests/test2.yaml compile - - esphome tests/test3.yaml compile - env: TARGET=Cpp-Lint dist: trusty sudo: required diff --git a/docker/Dockerfile.lint b/docker/Dockerfile.lint index 5d8893bdbe..2d77502dc2 100644 --- a/docker/Dockerfile.lint +++ b/docker/Dockerfile.lint @@ -14,7 +14,5 @@ RUN \ COPY requirements_test.txt /requirements_test.txt RUN pip3 install --no-cache-dir wheel && pip3 install --no-cache-dir -r /requirements_test.txt -RUN ln -s /usr/bin/pip3 /usr/bin/pip && ln -f -s /usr/bin/python3 /usr/bin/python - VOLUME ["/esphome"] WORKDIR /esphome diff --git a/esphome/__main__.py b/esphome/__main__.py index ae2db3e35c..73723dfa00 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -1,5 +1,3 @@ -from __future__ import print_function - import argparse import functools import logging @@ -14,7 +12,6 @@ from esphome.const import CONF_BAUD_RATE, CONF_BROKER, CONF_LOGGER, CONF_OTA, \ CONF_PASSWORD, CONF_PORT, CONF_ESPHOME, CONF_PLATFORMIO_OPTIONS from esphome.core import CORE, EsphomeError, coroutine, coroutine_with_priority from esphome.helpers import color, indent -from esphome.py_compat import IS_PY2, safe_input, IS_PY3 from esphome.util import run_external_command, run_external_process, safe_print, list_yaml_files _LOGGER = logging.getLogger(__name__) @@ -42,12 +39,12 @@ def choose_prompt(options): if len(options) == 1: return options[0][1] - safe_print(u"Found multiple options, please choose one:") + safe_print("Found multiple options, please choose one:") for i, (desc, _) in enumerate(options): - safe_print(u" [{}] {}".format(i + 1, desc)) + safe_print(f" [{i+1}] {desc}") while True: - opt = safe_input('(number): ') + opt = input('(number): ') if opt in options: opt = options.index(opt) break @@ -57,20 +54,20 @@ def choose_prompt(options): raise ValueError break except ValueError: - safe_print(color('red', u"Invalid option: '{}'".format(opt))) + safe_print(color('red', f"Invalid option: '{opt}'")) return options[opt - 1][1] def choose_upload_log_host(default, check_default, show_ota, show_mqtt, show_api): options = [] for res, desc in get_serial_ports(): - options.append((u"{} ({})".format(res, desc), res)) + options.append((f"{res} ({desc})", res)) if (show_ota and 'ota' in CORE.config) or (show_api and 'api' in CORE.config): - options.append((u"Over The Air ({})".format(CORE.address), CORE.address)) + options.append((f"Over The Air ({CORE.address})", CORE.address)) if default == 'OTA': return CORE.address if show_mqtt and 'mqtt' in CORE.config: - options.append((u"MQTT ({})".format(CORE.config['mqtt'][CONF_BROKER]), 'MQTT')) + options.append(("MQTT ({})".format(CORE.config['mqtt'][CONF_BROKER]), 'MQTT')) if default == 'OTA': return 'MQTT' if default is not None: @@ -108,11 +105,7 @@ def run_miniterm(config, port): except serial.SerialException: _LOGGER.error("Serial port closed!") return - if IS_PY2: - line = raw.replace('\r', '').replace('\n', '') - else: - line = raw.replace(b'\r', b'').replace(b'\n', b'').decode('utf8', - 'backslashreplace') + line = raw.replace(b'\r', b'').replace(b'\n', b'').decode('utf8', 'backslashreplace') time = datetime.now().time().strftime('[%H:%M:%S]') message = time + line safe_print(message) @@ -127,11 +120,9 @@ def wrap_to_code(name, comp): @functools.wraps(comp.to_code) @coroutine_with_priority(coro.priority) def wrapped(conf): - cg.add(cg.LineComment(u"{}:".format(name))) + cg.add(cg.LineComment(f"{name}:")) if comp.config_schema is not None: conf_str = yaml_util.dump(conf) - if IS_PY2: - conf_str = conf_str.decode('utf-8') conf_str = conf_str.replace('//', '') cg.add(cg.LineComment(indent(conf_str))) yield coro(conf) @@ -243,7 +234,7 @@ def setup_log(debug=False, quiet=False): log_level = logging.INFO logging.basicConfig(level=log_level) fmt = "%(levelname)s %(message)s" - colorfmt = "%(log_color)s{}%(reset)s".format(fmt) + colorfmt = f"%(log_color)s{fmt}%(reset)s" datefmt = '%H:%M:%S' logging.getLogger('urllib3').setLevel(logging.WARNING) @@ -292,12 +283,12 @@ def command_compile(args, config): if exit_code != 0: return exit_code if args.only_generate: - _LOGGER.info(u"Successfully generated source code.") + _LOGGER.info("Successfully generated source code.") return 0 exit_code = compile_program(args, config) if exit_code != 0: return exit_code - _LOGGER.info(u"Successfully compiled program.") + _LOGGER.info("Successfully compiled program.") return 0 @@ -307,7 +298,7 @@ def command_upload(args, config): exit_code = upload_program(config, args, port) if exit_code != 0: return exit_code - _LOGGER.info(u"Successfully uploaded program.") + _LOGGER.info("Successfully uploaded program.") return 0 @@ -324,13 +315,13 @@ def command_run(args, config): exit_code = compile_program(args, config) if exit_code != 0: return exit_code - _LOGGER.info(u"Successfully compiled program.") + _LOGGER.info("Successfully compiled program.") port = choose_upload_log_host(default=args.upload_port, check_default=None, show_ota=True, show_mqtt=False, show_api=True) exit_code = upload_program(config, args, port) if exit_code != 0: return exit_code - _LOGGER.info(u"Successfully uploaded program.") + _LOGGER.info("Successfully uploaded program.") if args.no_logs: return 0 port = choose_upload_log_host(default=args.upload_port, check_default=port, @@ -349,7 +340,7 @@ def command_mqtt_fingerprint(args, config): def command_version(args): - safe_print(u"Version: {}".format(const.__version__)) + safe_print(f"Version: {const.__version__}") return 0 @@ -377,10 +368,10 @@ def command_update_all(args): twidth = 60 def print_bar(middle_text): - middle_text = " {} ".format(middle_text) + middle_text = f" {middle_text} " width = len(click.unstyle(middle_text)) half_line = "=" * ((twidth - width) // 2) - click.echo("%s%s%s" % (half_line, middle_text, half_line)) + click.echo(f"{half_line}{middle_text}{half_line}") for f in files: print("Updating {}".format(color('cyan', f))) @@ -431,7 +422,7 @@ POST_CONFIG_ACTIONS = { def parse_args(argv): - parser = argparse.ArgumentParser(description='ESPHome v{}'.format(const.__version__)) + parser = argparse.ArgumentParser(description=f'ESPHome v{const.__version__}') parser.add_argument('-v', '--verbose', help="Enable verbose esphome logs.", action='store_true') parser.add_argument('-q', '--quiet', help="Disable all esphome logs.", @@ -525,14 +516,10 @@ def run_esphome(argv): _LOGGER.error("Missing configuration parameter, see esphome --help.") return 1 - if IS_PY2: - _LOGGER.warning("You're using ESPHome with python 2. Support for python 2 is deprecated " - "and will be removed in 1.15.0. Please reinstall ESPHome with python 3.6 " - "or higher.") - elif IS_PY3 and sys.version_info < (3, 6, 0): - _LOGGER.warning("You're using ESPHome with python 3.5. Support for python 3.5 is " - "deprecated and will be removed in 1.15.0. Please reinstall ESPHome with " - "python 3.6 or higher.") + if sys.version_info < (3, 6, 0): + _LOGGER.error("You're running ESPHome with Python <3.6. ESPHome is no longer compatible " + "with this Python version. Please reinstall ESPHome with Python 3.6+") + return 1 if args.command in PRE_CONFIG_ACTIONS: try: @@ -551,7 +538,7 @@ def run_esphome(argv): CORE.config = config if args.command not in POST_CONFIG_ACTIONS: - safe_print(u"Unknown command {}".format(args.command)) + safe_print(f"Unknown command {args.command}") try: rc = POST_CONFIG_ACTIONS[args.command](args, config) diff --git a/esphome/api/client.py b/esphome/api/client.py index 0c52674287..fcea90e3b4 100644 --- a/esphome/api/client.py +++ b/esphome/api/client.py @@ -14,7 +14,6 @@ import esphome.api.api_pb2 as pb from esphome.const import CONF_PASSWORD, CONF_PORT from esphome.core import EsphomeError from esphome.helpers import resolve_ip_address, indent, color -from esphome.py_compat import text_type, IS_PY2, byte_to_bytes, char_to_byte from esphome.util import safe_print _LOGGER = logging.getLogger(__name__) @@ -67,16 +66,16 @@ MESSAGE_TYPE_TO_PROTO = { def _varuint_to_bytes(value): if value <= 0x7F: - return byte_to_bytes(value) + return bytes([value]) ret = bytes() while value: temp = value & 0x7F value >>= 7 if value: - ret += byte_to_bytes(temp | 0x80) + ret += bytes([temp | 0x80]) else: - ret += byte_to_bytes(temp) + ret += bytes([temp]) return ret @@ -84,8 +83,7 @@ def _varuint_to_bytes(value): def _bytes_to_varuint(value): result = 0 bitpos = 0 - for c in value: - val = char_to_byte(c) + for val in value: result |= (val & 0x7F) << bitpos bitpos += 7 if (val & 0x80) == 0: @@ -191,8 +189,8 @@ class APIClient(threading.Thread): self._socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) try: self._socket.connect((ip, self._port)) - except socket.error as err: - err = APIConnectionError("Error connecting to {}: {}".format(ip, err)) + except OSError as err: + err = APIConnectionError(f"Error connecting to {ip}: {err}") self._fatal_error(err) raise err self._socket.settimeout(0.1) @@ -200,7 +198,7 @@ class APIClient(threading.Thread): self._socket_open_event.set() hello = pb.HelloRequest() - hello.client_info = 'ESPHome v{}'.format(const.__version__) + hello.client_info = f'ESPHome v{const.__version__}' try: resp = self._send_message_await_response(hello, pb.HelloResponse) except APIConnectionError as err: @@ -251,8 +249,8 @@ class APIClient(threading.Thread): with self._socket_write_lock: try: self._socket.sendall(data) - except socket.error as err: - err = APIConnectionError("Error while writing data: {}".format(err)) + except OSError as err: + err = APIConnectionError(f"Error while writing data: {err}") self._fatal_error(err) raise err @@ -265,11 +263,8 @@ class APIClient(threading.Thread): raise ValueError encoded = msg.SerializeToString() - _LOGGER.debug("Sending %s:\n%s", type(msg), indent(text_type(msg))) - if IS_PY2: - req = chr(0x00) - else: - req = bytes([0]) + _LOGGER.debug("Sending %s:\n%s", type(msg), indent(str(msg))) + req = bytes([0]) req += _varuint_to_bytes(len(encoded)) req += _varuint_to_bytes(message_type) req += encoded @@ -355,14 +350,14 @@ class APIClient(threading.Thread): raise APIConnectionError("Socket was closed") except socket.timeout: continue - except socket.error as err: - raise APIConnectionError("Error while receiving data: {}".format(err)) + except OSError as err: + raise APIConnectionError(f"Error while receiving data: {err}") ret += val return ret def _recv_varint(self): raw = bytes() - while not raw or char_to_byte(raw[-1]) & 0x80: + while not raw or raw[-1] & 0x80: raw += self._recv(1) return _bytes_to_varuint(raw) @@ -371,7 +366,7 @@ class APIClient(threading.Thread): return # Preamble - if char_to_byte(self._recv(1)[0]) != 0x00: + if self._recv(1)[0] != 0x00: raise APIConnectionError("Invalid preamble") length = self._recv_varint() @@ -436,7 +431,7 @@ def run_logs(config, address): return if err: - _LOGGER.warning(u"Disconnected from API: %s", err) + _LOGGER.warning("Disconnected from API: %s", err) while retry_timer: retry_timer.pop(0).cancel() @@ -454,18 +449,18 @@ def run_logs(config, address): wait_time = int(min(1.5**min(tries, 100), 30)) if not has_connects: - _LOGGER.warning(u"Initial connection failed. The ESP might not be connected " - u"to WiFi yet (%s). Re-Trying in %s seconds", + _LOGGER.warning("Initial connection failed. The ESP might not be connected " + "to WiFi yet (%s). Re-Trying in %s seconds", error, wait_time) else: - _LOGGER.warning(u"Couldn't connect to API (%s). Trying to reconnect in %s seconds", + _LOGGER.warning("Couldn't connect to API (%s). Trying to reconnect in %s seconds", error, wait_time) timer = threading.Timer(wait_time, functools.partial(try_connect, None, tries + 1)) timer.start() retry_timer.append(timer) def on_log(msg): - time_ = datetime.now().time().strftime(u'[%H:%M:%S]') + time_ = datetime.now().time().strftime('[%H:%M:%S]') text = msg.message if msg.send_failed: text = color('white', '(Message skipped because it was too big to fit in ' diff --git a/esphome/automation.py b/esphome/automation.py index f758191268..5df884e7c2 100644 --- a/esphome/automation.py +++ b/esphome/automation.py @@ -83,9 +83,9 @@ def validate_automation(extra_schema=None, extra_validators=None, single=False): try: return cv.Schema([schema])(value) except cv.Invalid as err2: - if u'extra keys not allowed' in str(err2) and len(err2.path) == 2: + if 'extra keys not allowed' in str(err2) and len(err2.path) == 2: raise err - if u'Unable to find action' in str(err): + if 'Unable to find action' in str(err): raise err2 raise cv.MultipleInvalid([err, err2]) elif isinstance(value, dict): diff --git a/esphome/components/ade7953/sensor.py b/esphome/components/ade7953/sensor.py index 4fcd307332..b048b1ed71 100644 --- a/esphome/components/ade7953/sensor.py +++ b/esphome/components/ade7953/sensor.py @@ -36,4 +36,4 @@ def to_code(config): continue conf = config[key] sens = yield sensor.new_sensor(conf) - cg.add(getattr(var, 'set_{}_sensor'.format(key))(sens)) + cg.add(getattr(var, f'set_{key}_sensor')(sens)) diff --git a/esphome/components/ads1115/sensor.py b/esphome/components/ads1115/sensor.py index 204ccb99d7..55619b22e9 100644 --- a/esphome/components/ads1115/sensor.py +++ b/esphome/components/ads1115/sensor.py @@ -2,7 +2,6 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import sensor, voltage_sampler from esphome.const import CONF_GAIN, CONF_MULTIPLEXER, ICON_FLASH, UNIT_VOLT, CONF_ID -from esphome.py_compat import string_types from . import ads1115_ns, ADS1115Component DEPENDENCIES = ['ads1115'] @@ -32,9 +31,9 @@ GAIN = { def validate_gain(value): if isinstance(value, float): - value = u'{:0.03f}'.format(value) - elif not isinstance(value, string_types): - raise cv.Invalid('invalid gain "{}"'.format(value)) + value = f'{value:0.03f}' + elif not isinstance(value, str): + raise cv.Invalid(f'invalid gain "{value}"') return cv.enum(GAIN)(value) diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py index a0568863ad..eef60602ba 100644 --- a/esphome/components/api/__init__.py +++ b/esphome/components/api/__init__.py @@ -102,7 +102,7 @@ def homeassistant_service_to_code(config, action_id, template_arg, args): def validate_homeassistant_event(value): value = cv.string(value) - if not value.startswith(u'esphome.'): + if not value.startswith('esphome.'): raise cv.Invalid("ESPHome can only generate Home Assistant events that begin with " "esphome. For example 'esphome.xyz'") return value diff --git a/esphome/components/binary_sensor/__init__.py b/esphome/components/binary_sensor/__init__.py index c082e2e9af..7c78c3a369 100644 --- a/esphome/components/binary_sensor/__init__.py +++ b/esphome/components/binary_sensor/__init__.py @@ -9,7 +9,6 @@ from esphome.const import CONF_DEVICE_CLASS, CONF_FILTERS, \ CONF_ON_DOUBLE_CLICK, CONF_ON_MULTI_CLICK, CONF_ON_PRESS, CONF_ON_RELEASE, CONF_ON_STATE, \ CONF_STATE, CONF_TIMING, CONF_TRIGGER_ID, CONF_FOR, CONF_NAME, CONF_MQTT_ID from esphome.core import CORE, coroutine, coroutine_with_priority -from esphome.py_compat import string_types from esphome.util import Registry DEVICE_CLASSES = [ @@ -94,7 +93,7 @@ MULTI_CLICK_TIMING_SCHEMA = cv.Schema({ def parse_multi_click_timing_str(value): - if not isinstance(value, string_types): + if not isinstance(value, str): return value parts = value.lower().split(' ') @@ -104,10 +103,10 @@ def parse_multi_click_timing_str(value): try: state = cv.boolean(parts[0]) except cv.Invalid: - raise cv.Invalid(u"First word must either be ON or OFF, not {}".format(parts[0])) + raise cv.Invalid("First word must either be ON or OFF, not {}".format(parts[0])) if parts[1] != 'for': - raise cv.Invalid(u"Second word must be 'for', got {}".format(parts[1])) + raise cv.Invalid("Second word must be 'for', got {}".format(parts[1])) if parts[2] == 'at': if parts[3] == 'least': @@ -115,12 +114,12 @@ def parse_multi_click_timing_str(value): elif parts[3] == 'most': key = CONF_MAX_LENGTH else: - raise cv.Invalid(u"Third word after at must either be 'least' or 'most', got {}" - u"".format(parts[3])) + raise cv.Invalid("Third word after at must either be 'least' or 'most', got {}" + "".format(parts[3])) try: length = cv.positive_time_period_milliseconds(parts[4]) except cv.Invalid as err: - raise cv.Invalid(u"Multi Click Grammar Parsing length failed: {}".format(err)) + raise cv.Invalid(f"Multi Click Grammar Parsing length failed: {err}") return { CONF_STATE: state, key: str(length) @@ -132,12 +131,12 @@ def parse_multi_click_timing_str(value): try: min_length = cv.positive_time_period_milliseconds(parts[2]) except cv.Invalid as err: - raise cv.Invalid(u"Multi Click Grammar Parsing minimum length failed: {}".format(err)) + raise cv.Invalid(f"Multi Click Grammar Parsing minimum length failed: {err}") try: max_length = cv.positive_time_period_milliseconds(parts[4]) except cv.Invalid as err: - raise cv.Invalid(u"Multi Click Grammar Parsing minimum length failed: {}".format(err)) + raise cv.Invalid(f"Multi Click Grammar Parsing minimum length failed: {err}") return { CONF_STATE: state, diff --git a/esphome/components/deep_sleep/__init__.py b/esphome/components/deep_sleep/__init__.py index 07c1d117b6..1b766c9928 100644 --- a/esphome/components/deep_sleep/__init__.py +++ b/esphome/components/deep_sleep/__init__.py @@ -8,8 +8,8 @@ from esphome.const import CONF_ID, CONF_MODE, CONF_NUMBER, CONF_PINS, CONF_RUN_C def validate_pin_number(value): valid_pins = [0, 2, 4, 12, 13, 14, 15, 25, 26, 27, 32, 33, 34, 35, 36, 37, 38, 39] if value[CONF_NUMBER] not in valid_pins: - raise cv.Invalid(u"Only pins {} support wakeup" - u"".format(', '.join(str(x) for x in valid_pins))) + raise cv.Invalid("Only pins {} support wakeup" + "".format(', '.join(str(x) for x in valid_pins))) return value diff --git a/esphome/components/display/__init__.py b/esphome/components/display/__init__.py index 38d19d832e..951d561caa 100644 --- a/esphome/components/display/__init__.py +++ b/esphome/components/display/__init__.py @@ -1,4 +1,3 @@ -# coding=utf-8 import esphome.codegen as cg import esphome.config_validation as cv from esphome import core, automation @@ -27,7 +26,7 @@ DISPLAY_ROTATIONS = { def validate_rotation(value): value = cv.string(value) - if value.endswith(u"°"): + if value.endswith("°"): value = value[:-1] return cv.enum(DISPLAY_ROTATIONS, int=True)(value) diff --git a/esphome/components/esp32_ble_tracker/__init__.py b/esphome/components/esp32_ble_tracker/__init__.py index ccc15eb451..3311801b6c 100644 --- a/esphome/components/esp32_ble_tracker/__init__.py +++ b/esphome/components/esp32_ble_tracker/__init__.py @@ -46,33 +46,33 @@ def bt_uuid(value): pattern = re.compile("^[A-F|0-9]{4,}$") if not pattern.match(value): raise cv.Invalid( - u"Invalid hexadecimal value for 16 bit UUID format: '{}'".format(in_value)) + f"Invalid hexadecimal value for 16 bit UUID format: '{in_value}'") return value if len(value) == len(bt_uuid32_format): pattern = re.compile("^[A-F|0-9]{8,}$") if not pattern.match(value): raise cv.Invalid( - u"Invalid hexadecimal value for 32 bit UUID format: '{}'".format(in_value)) + f"Invalid hexadecimal value for 32 bit UUID format: '{in_value}'") return value if len(value) == len(bt_uuid128_format): pattern = re.compile( "^[A-F|0-9]{8,}-[A-F|0-9]{4,}-[A-F|0-9]{4,}-[A-F|0-9]{4,}-[A-F|0-9]{12,}$") if not pattern.match(value): raise cv.Invalid( - u"Invalid hexadecimal value for 128 UUID format: '{}'".format(in_value)) + f"Invalid hexadecimal value for 128 UUID format: '{in_value}'") return value raise cv.Invalid( - u"Service UUID must be in 16 bit '{}', 32 bit '{}', or 128 bit '{}' format".format( + "Service UUID must be in 16 bit '{}', 32 bit '{}', or 128 bit '{}' format".format( bt_uuid16_format, bt_uuid32_format, bt_uuid128_format)) def as_hex(value): - return cg.RawExpression('0x{}ULL'.format(value)) + return cg.RawExpression(f'0x{value}ULL') def as_hex_array(value): value = value.replace("-", "") - cpp_array = ['0x{}'.format(part) for part in [value[i:i+2] for i in range(0, len(value), 2)]] + cpp_array = [f'0x{part}' for part in [value[i:i+2] for i in range(0, len(value), 2)]] return cg.RawExpression( '(uint8_t*)(const uint8_t[16]){{{}}}'.format(','.join(reversed(cpp_array)))) diff --git a/esphome/components/esp32_touch/binary_sensor.py b/esphome/components/esp32_touch/binary_sensor.py index a72ca5796f..5142879a04 100644 --- a/esphome/components/esp32_touch/binary_sensor.py +++ b/esphome/components/esp32_touch/binary_sensor.py @@ -27,7 +27,7 @@ TOUCH_PADS = { def validate_touch_pad(value): value = validate_gpio_pin(value) if value not in TOUCH_PADS: - raise cv.Invalid("Pin {} does not support touch pads.".format(value)) + raise cv.Invalid(f"Pin {value} does not support touch pads.") return value diff --git a/esphome/components/font/__init__.py b/esphome/components/font/__init__.py index e77bd5da8e..5b19dc74e0 100644 --- a/esphome/components/font/__init__.py +++ b/esphome/components/font/__init__.py @@ -1,11 +1,11 @@ -# coding=utf-8 +import functools + from esphome import core from esphome.components import display import esphome.config_validation as cv import esphome.codegen as cg from esphome.const import CONF_FILE, CONF_GLYPHS, CONF_ID, CONF_SIZE from esphome.core import CORE, HexInt -from esphome.py_compat import sort_by_cmp DEPENDENCIES = ['display'] MULTI_CONF = True @@ -33,9 +33,9 @@ def validate_glyphs(value): return -1 if len(x_) > len(y_): return 1 - raise cv.Invalid(u"Found duplicate glyph {}".format(x)) + raise cv.Invalid(f"Found duplicate glyph {x}") - sort_by_cmp(value, comparator) + value.sort(key=functools.cmp_to_key(comparator)) return value @@ -55,15 +55,15 @@ def validate_pillow_installed(value): def validate_truetype_file(value): if value.endswith('.zip'): # for Google Fonts downloads - raise cv.Invalid(u"Please unzip the font archive '{}' first and then use the .ttf files " - u"inside.".format(value)) + raise cv.Invalid("Please unzip the font archive '{}' first and then use the .ttf files " + "inside.".format(value)) if not value.endswith('.ttf'): - raise cv.Invalid(u"Only truetype (.ttf) files are supported. Please make sure you're " - u"using the correct format or rename the extension to .ttf") + raise cv.Invalid("Only truetype (.ttf) files are supported. Please make sure you're " + "using the correct format or rename the extension to .ttf") return cv.file_(value) -DEFAULT_GLYPHS = u' !"%()+,-.:0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz°' +DEFAULT_GLYPHS = ' !"%()+,-.:0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz°' CONF_RAW_DATA_ID = 'raw_data_id' FONT_SCHEMA = cv.Schema({ @@ -84,7 +84,7 @@ def to_code(config): try: font = ImageFont.truetype(path, config[CONF_SIZE]) except Exception as e: - raise core.EsphomeError(u"Could not load truetype file {}: {}".format(path, e)) + raise core.EsphomeError(f"Could not load truetype file {path}: {e}") ascent, descent = font.getmetrics() diff --git a/esphome/components/globals/__init__.py b/esphome/components/globals/__init__.py index e7030978ed..d285f1e97f 100644 --- a/esphome/components/globals/__init__.py +++ b/esphome/components/globals/__init__.py @@ -4,7 +4,6 @@ from esphome import config_validation as cv, automation from esphome import codegen as cg from esphome.const import CONF_ID, CONF_INITIAL_VALUE, CONF_RESTORE_VALUE, CONF_TYPE, CONF_VALUE from esphome.core import coroutine_with_priority -from esphome.py_compat import IS_PY3 globals_ns = cg.esphome_ns.namespace('globals') GlobalsComponent = globals_ns.class_('GlobalsComponent', cg.Component) @@ -36,7 +35,7 @@ def to_code(config): if config[CONF_RESTORE_VALUE]: value = config[CONF_ID].id - if IS_PY3 and isinstance(value, str): + if isinstance(value, str): value = value.encode() hash_ = int(hashlib.md5(value).hexdigest()[:8], 16) cg.add(glob.set_restore_value(hash_)) diff --git a/esphome/components/hmc5883l/sensor.py b/esphome/components/hmc5883l/sensor.py index 6e2366cadc..b063284698 100644 --- a/esphome/components/hmc5883l/sensor.py +++ b/esphome/components/hmc5883l/sensor.py @@ -1,11 +1,9 @@ -# coding=utf-8 import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import i2c, sensor from esphome.const import (CONF_ADDRESS, CONF_ID, CONF_OVERSAMPLING, CONF_RANGE, ICON_MAGNET, UNIT_MICROTESLA, UNIT_DEGREES, ICON_SCREEN_ROTATION, CONF_UPDATE_INTERVAL) -from esphome.py_compat import text_type DEPENDENCIES = ['i2c'] @@ -54,7 +52,7 @@ def validate_enum(enum_values, units=None, int=True): _units = [] if units is not None: _units = units if isinstance(units, list) else [units] - _units = [text_type(x) for x in _units] + _units = [str(x) for x in _units] enum_bound = cv.enum(enum_values, int=int) def validate_enum_bound(value): @@ -74,7 +72,7 @@ CONFIG_SCHEMA = cv.Schema({ cv.GenerateID(): cv.declare_id(HMC5883LComponent), cv.Optional(CONF_ADDRESS): cv.i2c_address, cv.Optional(CONF_OVERSAMPLING, default='1x'): validate_enum(HMC5883LOversamplings, units="x"), - cv.Optional(CONF_RANGE, default=u'130µT'): validate_enum(HMC5883L_RANGES, units=["uT", u"µT"]), + cv.Optional(CONF_RANGE, default='130µT'): validate_enum(HMC5883L_RANGES, units=["uT", "µT"]), cv.Optional(CONF_FIELD_STRENGTH_X): field_strength_schema, cv.Optional(CONF_FIELD_STRENGTH_Y): field_strength_schema, cv.Optional(CONF_FIELD_STRENGTH_Z): field_strength_schema, diff --git a/esphome/components/http_request/__init__.py b/esphome/components/http_request/__init__.py index 23fc38ba40..897440a454 100644 --- a/esphome/components/http_request/__init__.py +++ b/esphome/components/http_request/__init__.py @@ -1,3 +1,5 @@ +import urllib.parse as urlparse + import esphome.codegen as cg import esphome.config_validation as cv from esphome import automation @@ -5,12 +7,6 @@ from esphome.const import CONF_ID, CONF_TIMEOUT, CONF_ESPHOME, CONF_METHOD, \ CONF_ARDUINO_VERSION, ARDUINO_VERSION_ESP8266_2_5_0 from esphome.core import CORE, Lambda from esphome.core_config import PLATFORMIO_ESP8266_LUT -from esphome.py_compat import IS_PY3 - -if IS_PY3: - import urllib.parse as urlparse # pylint: disable=no-name-in-module,import-error -else: - import urlparse # pylint: disable=import-error DEPENDENCIES = ['network'] AUTO_LOAD = ['json'] diff --git a/esphome/components/image/__init__.py b/esphome/components/image/__init__.py index d933af1a93..d0a41a7379 100644 --- a/esphome/components/image/__init__.py +++ b/esphome/components/image/__init__.py @@ -1,4 +1,3 @@ -# coding=utf-8 import logging from esphome import core @@ -33,7 +32,7 @@ def to_code(config): try: image = Image.open(path) except Exception as e: - raise core.EsphomeError(u"Could not load image file {}: {}".format(path, e)) + raise core.EsphomeError(f"Could not load image file {path}: {e}") if CONF_RESIZE in config: image.thumbnail(config[CONF_RESIZE]) diff --git a/esphome/components/ina219/sensor.py b/esphome/components/ina219/sensor.py index a6f415edb0..6a61e16226 100644 --- a/esphome/components/ina219/sensor.py +++ b/esphome/components/ina219/sensor.py @@ -1,4 +1,3 @@ -# coding=utf-8 import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import i2c, sensor diff --git a/esphome/components/ina226/sensor.py b/esphome/components/ina226/sensor.py index 02a13f98c4..066363b3d4 100644 --- a/esphome/components/ina226/sensor.py +++ b/esphome/components/ina226/sensor.py @@ -1,4 +1,3 @@ -# coding=utf-8 import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import i2c, sensor diff --git a/esphome/components/ina3221/sensor.py b/esphome/components/ina3221/sensor.py index 199f7be624..1c26533cc4 100644 --- a/esphome/components/ina3221/sensor.py +++ b/esphome/components/ina3221/sensor.py @@ -1,4 +1,3 @@ -# coding=utf-8 import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import i2c, sensor diff --git a/esphome/components/light/effects.py b/esphome/components/light/effects.py index c2250e7e0c..005a66a5ad 100644 --- a/esphome/components/light/effects.py +++ b/esphome/components/light/effects.py @@ -253,8 +253,8 @@ def validate_effects(allowed_effects): name = x[key][CONF_NAME] if name in names: errors.append( - cv.Invalid(u"Found the effect name '{}' twice. All effects must have " - u"unique names".format(name), [i]) + cv.Invalid("Found the effect name '{}' twice. All effects must have " + "unique names".format(name), [i]) ) continue names.add(name) diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index 850f955f65..329c515aee 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -7,7 +7,6 @@ from esphome.automation import LambdaAction from esphome.const import CONF_ARGS, CONF_BAUD_RATE, CONF_FORMAT, CONF_HARDWARE_UART, CONF_ID, \ CONF_LEVEL, CONF_LOGS, CONF_ON_MESSAGE, CONF_TAG, CONF_TRIGGER_ID, CONF_TX_BUFFER_SIZE from esphome.core import CORE, EsphomeError, Lambda, coroutine_with_priority -from esphome.py_compat import text_type logger_ns = cg.esphome_ns.namespace('logger') LOG_LEVELS = { @@ -64,8 +63,8 @@ def validate_local_no_higher_than_global(value): global_level = value.get(CONF_LEVEL, 'DEBUG') for tag, level in value.get(CONF_LOGS, {}).items(): if LOG_LEVEL_SEVERITY.index(level) > LOG_LEVEL_SEVERITY.index(global_level): - raise EsphomeError(u"The local log level {} for {} must be less severe than the " - u"global log level {}.".format(level, tag, global_level)) + raise EsphomeError("The local log level {} for {} must be less severe than the " + "global log level {}.".format(level, tag, global_level)) return value @@ -119,7 +118,7 @@ def to_code(config): if CORE.is_esp8266 and has_serial_logging and is_at_least_verbose: debug_serial_port = HARDWARE_UART_TO_SERIAL[config.get(CONF_HARDWARE_UART)] - cg.add_build_flag("-DDEBUG_ESP_PORT={}".format(debug_serial_port)) + cg.add_build_flag(f"-DDEBUG_ESP_PORT={debug_serial_port}") cg.add_build_flag("-DLWIP_DEBUG") DEBUG_COMPONENTS = { 'HTTP_CLIENT', @@ -134,7 +133,7 @@ def to_code(config): # 'MDNS_RESPONDER', } for comp in DEBUG_COMPONENTS: - cg.add_build_flag("-DDEBUG_ESP_{}".format(comp)) + cg.add_build_flag(f"-DDEBUG_ESP_{comp}") if CORE.is_esp32 and is_at_least_verbose: cg.add_build_flag('-DCORE_DEBUG_LEVEL=5') if CORE.is_esp32 and is_at_least_very_verbose: @@ -165,7 +164,7 @@ def maybe_simple_message(schema): def validate_printf(value): # https://stackoverflow.com/questions/30011379/how-can-i-parse-a-c-format-string-in-python # pylint: disable=anomalous-backslash-in-string - cfmt = u"""\ + cfmt = """\ ( # start of capture group 1 % # literal "%" (?: # first option @@ -179,8 +178,8 @@ def validate_printf(value): """ # noqa matches = re.findall(cfmt, value[CONF_FORMAT], flags=re.X) if len(matches) != len(value[CONF_ARGS]): - raise cv.Invalid(u"Found {} printf-patterns ({}), but {} args were given!" - u"".format(len(matches), u', '.join(matches), len(value[CONF_ARGS]))) + raise cv.Invalid("Found {} printf-patterns ({}), but {} args were given!" + "".format(len(matches), ', '.join(matches), len(value[CONF_ARGS]))) return value @@ -196,9 +195,9 @@ LOGGER_LOG_ACTION_SCHEMA = cv.All(maybe_simple_message({ @automation.register_action(CONF_LOGGER_LOG, LambdaAction, LOGGER_LOG_ACTION_SCHEMA) def logger_log_action_to_code(config, action_id, template_arg, args): esp_log = LOG_LEVEL_TO_ESP_LOG[config[CONF_LEVEL]] - args_ = [cg.RawExpression(text_type(x)) for x in config[CONF_ARGS]] + args_ = [cg.RawExpression(str(x)) for x in config[CONF_ARGS]] - text = text_type(cg.statement(esp_log(config[CONF_TAG], config[CONF_FORMAT], *args_))) + text = str(cg.statement(esp_log(config[CONF_TAG], config[CONF_FORMAT], *args_))) lambda_ = yield cg.process_lambda(Lambda(text), args, return_type=cg.void) yield cg.new_Pvariable(action_id, template_arg, lambda_) diff --git a/esphome/components/mpu6050/sensor.py b/esphome/components/mpu6050/sensor.py index bf44c67848..73c78e7f16 100644 --- a/esphome/components/mpu6050/sensor.py +++ b/esphome/components/mpu6050/sensor.py @@ -39,14 +39,14 @@ def to_code(config): yield i2c.register_i2c_device(var, config) for d in ['x', 'y', 'z']: - accel_key = 'accel_{}'.format(d) + accel_key = f'accel_{d}' if accel_key in config: sens = yield sensor.new_sensor(config[accel_key]) - cg.add(getattr(var, 'set_accel_{}_sensor'.format(d))(sens)) - accel_key = 'gyro_{}'.format(d) + cg.add(getattr(var, f'set_accel_{d}_sensor')(sens)) + accel_key = f'gyro_{d}' if accel_key in config: sens = yield sensor.new_sensor(config[accel_key]) - cg.add(getattr(var, 'set_gyro_{}_sensor'.format(d))(sens)) + cg.add(getattr(var, f'set_gyro_{d}_sensor')(sens)) if CONF_TEMPERATURE in config: sens = yield sensor.new_sensor(config[CONF_TEMPERATURE]) diff --git a/esphome/components/mqtt/__init__.py b/esphome/components/mqtt/__init__.py index 20d7f5aafe..2f0ed0f8e2 100644 --- a/esphome/components/mqtt/__init__.py +++ b/esphome/components/mqtt/__init__.py @@ -64,28 +64,28 @@ def validate_config(value): topic_prefix = value[CONF_TOPIC_PREFIX] if CONF_BIRTH_MESSAGE not in value: out[CONF_BIRTH_MESSAGE] = { - CONF_TOPIC: '{}/status'.format(topic_prefix), + CONF_TOPIC: f'{topic_prefix}/status', CONF_PAYLOAD: 'online', CONF_QOS: 0, CONF_RETAIN: True, } if CONF_WILL_MESSAGE not in value: out[CONF_WILL_MESSAGE] = { - CONF_TOPIC: '{}/status'.format(topic_prefix), + CONF_TOPIC: f'{topic_prefix}/status', CONF_PAYLOAD: 'offline', CONF_QOS: 0, CONF_RETAIN: True, } if CONF_SHUTDOWN_MESSAGE not in value: out[CONF_SHUTDOWN_MESSAGE] = { - CONF_TOPIC: '{}/status'.format(topic_prefix), + CONF_TOPIC: f'{topic_prefix}/status', CONF_PAYLOAD: 'offline', CONF_QOS: 0, CONF_RETAIN: True, } if CONF_LOG_TOPIC not in value: out[CONF_LOG_TOPIC] = { - CONF_TOPIC: '{}/debug'.format(topic_prefix), + CONF_TOPIC: f'{topic_prefix}/debug', CONF_QOS: 0, CONF_RETAIN: True, } @@ -95,7 +95,7 @@ def validate_config(value): def validate_fingerprint(value): value = cv.string(value) if re.match(r'^[0-9a-f]{40}$', value) is None: - raise cv.Invalid(u"fingerprint must be valid SHA1 hash") + raise cv.Invalid("fingerprint must be valid SHA1 hash") return value diff --git a/esphome/components/neopixelbus/light.py b/esphome/components/neopixelbus/light.py index e405b3d7a8..ea1b67f8ce 100644 --- a/esphome/components/neopixelbus/light.py +++ b/esphome/components/neopixelbus/light.py @@ -82,7 +82,7 @@ def validate_method_pin(value): for opt in (CONF_PIN, CONF_CLOCK_PIN, CONF_DATA_PIN): if opt in value and value[opt] not in pins_: raise cv.Invalid("Method {} only supports pin(s) {}".format( - method, ', '.join('GPIO{}'.format(x) for x in pins_) + method, ', '.join(f'GPIO{x}' for x in pins_) ), path=[CONF_METHOD]) return value diff --git a/esphome/components/ntc/sensor.py b/esphome/components/ntc/sensor.py index 7ff4f4e137..f2eb601ed2 100644 --- a/esphome/components/ntc/sensor.py +++ b/esphome/components/ntc/sensor.py @@ -1,4 +1,3 @@ -# coding=utf-8 from math import log import esphome.config_validation as cv @@ -28,7 +27,7 @@ def validate_calibration_parameter(value): value = cv.string(value) parts = value.split('->') if len(parts) != 2: - raise cv.Invalid(u"Calibration parameter must be of form 3000 -> 23°C") + raise cv.Invalid("Calibration parameter must be of form 3000 -> 23°C") voltage = cv.resistance(parts[0].strip()) temperature = cv.temperature(parts[1].strip()) return validate_calibration_parameter({ diff --git a/esphome/components/partition/light.py b/esphome/components/partition/light.py index 62434e7a19..ba1059e36b 100644 --- a/esphome/components/partition/light.py +++ b/esphome/components/partition/light.py @@ -10,8 +10,8 @@ PartitionLightOutput = partitions_ns.class_('PartitionLightOutput', light.Addres def validate_from_to(value): if value[CONF_FROM] > value[CONF_TO]: - raise cv.Invalid(u"From ({}) must not be larger than to ({})" - u"".format(value[CONF_FROM], value[CONF_TO])) + raise cv.Invalid("From ({}) must not be larger than to ({})" + "".format(value[CONF_FROM], value[CONF_TO])) return value diff --git a/esphome/components/pmsx003/sensor.py b/esphome/components/pmsx003/sensor.py index 0cbaf1bf29..4dbe500d3d 100644 --- a/esphome/components/pmsx003/sensor.py +++ b/esphome/components/pmsx003/sensor.py @@ -36,7 +36,7 @@ SENSORS_TO_TYPE = { def validate_pmsx003_sensors(value): for key, types in SENSORS_TO_TYPE.items(): if key in value and value[CONF_TYPE] not in types: - raise cv.Invalid(u"{} does not have {} sensor!".format(value[CONF_TYPE], key)) + raise cv.Invalid("{} does not have {} sensor!".format(value[CONF_TYPE], key)) return value diff --git a/esphome/components/qmc5883l/sensor.py b/esphome/components/qmc5883l/sensor.py index 8ccd542753..8a2952f54f 100644 --- a/esphome/components/qmc5883l/sensor.py +++ b/esphome/components/qmc5883l/sensor.py @@ -1,11 +1,9 @@ -# coding=utf-8 import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import i2c, sensor from esphome.const import (CONF_ADDRESS, CONF_ID, CONF_OVERSAMPLING, CONF_RANGE, ICON_MAGNET, UNIT_MICROTESLA, UNIT_DEGREES, ICON_SCREEN_ROTATION, CONF_UPDATE_INTERVAL) -from esphome.py_compat import text_type DEPENDENCIES = ['i2c'] @@ -46,7 +44,7 @@ def validate_enum(enum_values, units=None, int=True): _units = [] if units is not None: _units = units if isinstance(units, list) else [units] - _units = [text_type(x) for x in _units] + _units = [str(x) for x in _units] enum_bound = cv.enum(enum_values, int=int) def validate_enum_bound(value): @@ -65,7 +63,7 @@ heading_schema = sensor.sensor_schema(UNIT_DEGREES, ICON_SCREEN_ROTATION, 1) CONFIG_SCHEMA = cv.Schema({ cv.GenerateID(): cv.declare_id(QMC5883LComponent), cv.Optional(CONF_ADDRESS): cv.i2c_address, - cv.Optional(CONF_RANGE, default=u'200µT'): validate_enum(QMC5883L_RANGES, units=["uT", u"µT"]), + cv.Optional(CONF_RANGE, default='200µT'): validate_enum(QMC5883L_RANGES, units=["uT", "µT"]), cv.Optional(CONF_OVERSAMPLING, default="512x"): validate_enum(QMC5883LOversamplings, units="x"), cv.Optional(CONF_FIELD_STRENGTH_X): field_strength_schema, cv.Optional(CONF_FIELD_STRENGTH_Y): field_strength_schema, diff --git a/esphome/components/remote_base/__init__.py b/esphome/components/remote_base/__init__.py index 023f0f253e..3d57041e56 100644 --- a/esphome/components/remote_base/__init__.py +++ b/esphome/components/remote_base/__init__.py @@ -7,7 +7,6 @@ from esphome.const import CONF_DATA, CONF_TRIGGER_ID, CONF_NBITS, CONF_ADDRESS, CONF_PROTOCOL, CONF_GROUP, CONF_DEVICE, CONF_STATE, CONF_CHANNEL, CONF_FAMILY, CONF_REPEAT, \ CONF_WAIT_TIME, CONF_TIMES, CONF_TYPE_ID, CONF_CARRIER_FREQUENCY from esphome.core import coroutine -from esphome.py_compat import string_types, text_type from esphome.util import Registry, SimpleRegistry AUTO_LOAD = ['binary_sensor'] @@ -52,7 +51,7 @@ def register_trigger(name, type, data_type): cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(type), cv.GenerateID(CONF_RECEIVER_ID): cv.use_id(RemoteReceiverBase), }) - registerer = TRIGGER_REGISTRY.register('on_{}'.format(name), validator) + registerer = TRIGGER_REGISTRY.register(f'on_{name}', validator) def decorator(func): @coroutine @@ -98,7 +97,7 @@ def register_action(name, type_, schema): cv.GenerateID(CONF_TRANSMITTER_ID): cv.use_id(RemoteTransmitterBase), cv.Optional(CONF_REPEAT): validate_repeat, }) - registerer = automation.register_action('remote_transmitter.transmit_{}'.format(name), + registerer = automation.register_action(f'remote_transmitter.transmit_{name}', type_, validator) def decorator(func): @@ -122,11 +121,11 @@ def register_action(name, type_, schema): def declare_protocol(name): - data = ns.struct('{}Data'.format(name)) - binary_sensor_ = ns.class_('{}BinarySensor'.format(name), RemoteReceiverBinarySensorBase) - trigger = ns.class_('{}Trigger'.format(name), RemoteReceiverTrigger) - action = ns.class_('{}Action'.format(name), RemoteTransmitterActionBase) - dumper = ns.class_('{}Dumper'.format(name), RemoteTransmitterDumper) + data = ns.struct(f'{name}Data') + binary_sensor_ = ns.class_(f'{name}BinarySensor', RemoteReceiverBinarySensorBase) + trigger = ns.class_(f'{name}Trigger', RemoteReceiverTrigger) + action = ns.class_(f'{name}Action', RemoteTransmitterActionBase) + dumper = ns.class_(f'{name}Dumper', RemoteTransmitterDumper) return data, binary_sensor_, trigger, action, dumper @@ -141,7 +140,7 @@ DUMPER_REGISTRY = Registry({ def validate_dumpers(value): - if isinstance(value, string_types) and value.lower() == 'all': + if isinstance(value, str) and value.lower() == 'all': return validate_dumpers(list(DUMPER_REGISTRY.keys())) return cv.validate_registry('dumper', DUMPER_REGISTRY)(value) @@ -432,12 +431,12 @@ RC_SWITCH_PROTOCOL_SCHEMA = cv.Any( def validate_rc_switch_code(value): - if not isinstance(value, (str, text_type)): + if not isinstance(value, (str, str)): raise cv.Invalid("All RCSwitch codes must be in quotes ('')") for c in value: if c not in ('0', '1'): - raise cv.Invalid(u"Invalid RCSwitch code character '{}'. Only '0' and '1' are allowed" - u"".format(c)) + raise cv.Invalid("Invalid RCSwitch code character '{}'. Only '0' and '1' are allowed" + "".format(c)) if len(value) > 64: raise cv.Invalid("Maximum length for RCSwitch codes is 64, code '{}' has length {}" "".format(value, len(value))) @@ -447,7 +446,7 @@ def validate_rc_switch_code(value): def validate_rc_switch_raw_code(value): - if not isinstance(value, (str, text_type)): + if not isinstance(value, (str, str)): raise cv.Invalid("All RCSwitch raw codes must be in quotes ('')") for c in value: if c not in ('0', '1', 'x'): diff --git a/esphome/components/remote_transmitter/switch.py b/esphome/components/remote_transmitter/switch.py index 5802ece807..5e0be04d7a 100644 --- a/esphome/components/remote_transmitter/switch.py +++ b/esphome/components/remote_transmitter/switch.py @@ -19,12 +19,12 @@ def show_new(value): if 'name' in value: args.append(('name', value['name'])) args.append(('turn_on_action', { - 'remote_transmitter.transmit_{}'.format(key): val + f'remote_transmitter.transmit_{key}': val })) text = yaml_util.dump([OrderedDict(args)]) - raise cv.Invalid(u"This platform has been removed in 1.13, please change to:\n\n{}\n\n." - u"".format(text)) + raise cv.Invalid("This platform has been removed in 1.13, please change to:\n\n{}\n\n." + "".format(text)) CONFIG_SCHEMA = show_new diff --git a/esphome/components/stepper/__init__.py b/esphome/components/stepper/__init__.py index 087fb18137..e8d6acbd1c 100644 --- a/esphome/components/stepper/__init__.py +++ b/esphome/components/stepper/__init__.py @@ -28,7 +28,7 @@ def validate_acceleration(value): try: value = float(value) except ValueError: - raise cv.Invalid("Expected acceleration as floating point number, got {}".format(value)) + raise cv.Invalid(f"Expected acceleration as floating point number, got {value}") if value <= 0: raise cv.Invalid("Acceleration must be larger than 0 steps/s^2!") @@ -48,7 +48,7 @@ def validate_speed(value): try: value = float(value) except ValueError: - raise cv.Invalid("Expected speed as floating point number, got {}".format(value)) + raise cv.Invalid(f"Expected speed as floating point number, got {value}") if value <= 0: raise cv.Invalid("Speed must be larger than 0 steps/s!") diff --git a/esphome/components/substitutions/__init__.py b/esphome/components/substitutions/__init__.py index 433a3066ee..292a7bf299 100644 --- a/esphome/components/substitutions/__init__.py +++ b/esphome/components/substitutions/__init__.py @@ -3,7 +3,6 @@ import re import esphome.config_validation as cv from esphome import core -from esphome.py_compat import string_types _LOGGER = logging.getLogger(__name__) @@ -24,8 +23,8 @@ def validate_substitution_key(value): for char in value: if char not in VALID_SUBSTITUTIONS_CHARACTERS: raise cv.Invalid( - u"Substitution must only consist of upper/lowercase characters, the underscore " - u"and numbers. The character '{}' cannot be used".format(char)) + "Substitution must only consist of upper/lowercase characters, the underscore " + "and numbers. The character '{}' cannot be used".format(char)) return value @@ -42,7 +41,7 @@ VARIABLE_PROG = re.compile('\\$([{0}]+|\\{{[{0}]*\\}})'.format(VALID_SUBSTITUTIO def _expand_substitutions(substitutions, value, path): - if u'$' not in value: + if '$' not in value: return value orig_value = value @@ -56,11 +55,11 @@ def _expand_substitutions(substitutions, value, path): i, j = m.span(0) name = m.group(1) - if name.startswith(u'{') and name.endswith(u'}'): + if name.startswith('{') and name.endswith('}'): name = name[1:-1] if name not in substitutions: - _LOGGER.warning(u"Found '%s' (see %s) which looks like a substitution, but '%s' was " - u"not declared", orig_value, u'->'.join(str(x) for x in path), name) + _LOGGER.warning("Found '%s' (see %s) which looks like a substitution, but '%s' was " + "not declared", orig_value, '->'.join(str(x) for x in path), name) i = j continue @@ -91,7 +90,7 @@ def _substitute_item(substitutions, item, path): for old, new in replace_keys: item[new] = item[old] del item[old] - elif isinstance(item, string_types): + elif isinstance(item, str): sub = _expand_substitutions(substitutions, item, path) if sub != item: return sub @@ -109,8 +108,8 @@ def do_substitution_pass(config): substitutions = config[CONF_SUBSTITUTIONS] with cv.prepend_path('substitutions'): if not isinstance(substitutions, dict): - raise cv.Invalid(u"Substitutions must be a key to value mapping, got {}" - u"".format(type(substitutions))) + raise cv.Invalid("Substitutions must be a key to value mapping, got {}" + "".format(type(substitutions))) replace_keys = [] for key, value in substitutions.items(): diff --git a/esphome/components/sun/__init__.py b/esphome/components/sun/__init__.py index fef0902181..e4d2023a8e 100644 --- a/esphome/components/sun/__init__.py +++ b/esphome/components/sun/__init__.py @@ -3,7 +3,6 @@ import esphome.config_validation as cv from esphome import automation from esphome.components import time from esphome.const import CONF_TIME_ID, CONF_ID, CONF_TRIGGER_ID -from esphome.py_compat import string_types sun_ns = cg.esphome_ns.namespace('sun') @@ -32,7 +31,7 @@ ELEVATION_MAP = { def elevation(value): - if isinstance(value, string_types): + if isinstance(value, str): try: value = ELEVATION_MAP[cv.one_of(*ELEVATION_MAP, lower=True, space='_')(value)] except cv.Invalid: diff --git a/esphome/components/tcs34725/sensor.py b/esphome/components/tcs34725/sensor.py index 478fe4aba6..287b2e441c 100644 --- a/esphome/components/tcs34725/sensor.py +++ b/esphome/components/tcs34725/sensor.py @@ -1,4 +1,3 @@ -# coding=utf-8 import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import i2c, sensor diff --git a/esphome/components/time/__init__.py b/esphome/components/time/__init__.py index 58739772b0..6283392103 100644 --- a/esphome/components/time/__init__.py +++ b/esphome/components/time/__init__.py @@ -14,7 +14,6 @@ from esphome.const import CONF_CRON, CONF_DAYS_OF_MONTH, CONF_DAYS_OF_WEEK, CONF CONF_MINUTES, CONF_MONTHS, CONF_ON_TIME, CONF_SECONDS, CONF_TIMEZONE, CONF_TRIGGER_ID, \ CONF_AT, CONF_SECOND, CONF_HOUR, CONF_MINUTE from esphome.core import coroutine, coroutine_with_priority -from esphome.py_compat import string_types _LOGGER = logging.getLogger(__name__) @@ -33,10 +32,10 @@ def _tz_timedelta(td): if offset_hour == 0 and offset_minute == 0 and offset_second == 0: return '0' if offset_minute == 0 and offset_second == 0: - return '{}'.format(offset_hour) + return f'{offset_hour}' if offset_second == 0: - return '{}:{}'.format(offset_hour, offset_minute) - return '{}:{}:{}'.format(offset_hour, offset_minute, offset_second) + return f'{offset_hour}:{offset_minute}' + return f'{offset_hour}:{offset_minute}:{offset_second}' # https://stackoverflow.com/a/16804556/8924614 @@ -133,7 +132,7 @@ def detect_tz(): def _parse_cron_int(value, special_mapping, message): special_mapping = special_mapping or {} - if isinstance(value, string_types) and value in special_mapping: + if isinstance(value, str) and value in special_mapping: return special_mapping[value] try: return int(value) @@ -143,41 +142,40 @@ def _parse_cron_int(value, special_mapping, message): def _parse_cron_part(part, min_value, max_value, special_mapping): if part in ('*', '?'): - return set(x for x in range(min_value, max_value + 1)) + return set(range(min_value, max_value + 1)) if '/' in part: data = part.split('/') if len(data) > 2: - raise cv.Invalid(u"Can't have more than two '/' in one time expression, got {}" + raise cv.Invalid("Can't have more than two '/' in one time expression, got {}" .format(part)) offset, repeat = data offset_n = 0 if offset: offset_n = _parse_cron_int(offset, special_mapping, - u"Offset for '/' time expression must be an integer, got {}") + "Offset for '/' time expression must be an integer, got {}") try: repeat_n = int(repeat) except ValueError: - raise cv.Invalid(u"Repeat for '/' time expression must be an integer, got {}" + raise cv.Invalid("Repeat for '/' time expression must be an integer, got {}" .format(repeat)) - return set(x for x in range(offset_n, max_value + 1, repeat_n)) + return set(range(offset_n, max_value + 1, repeat_n)) if '-' in part: data = part.split('-') if len(data) > 2: - raise cv.Invalid(u"Can't have more than two '-' in range time expression '{}'" + raise cv.Invalid("Can't have more than two '-' in range time expression '{}'" .format(part)) begin, end = data - begin_n = _parse_cron_int(begin, special_mapping, u"Number for time range must be integer, " - u"got {}") - end_n = _parse_cron_int(end, special_mapping, u"Number for time range must be integer, " - u"got {}") + begin_n = _parse_cron_int(begin, special_mapping, "Number for time range must be integer, " + "got {}") + end_n = _parse_cron_int(end, special_mapping, "Number for time range must be integer, " + "got {}") if end_n < begin_n: - return set(x for x in range(end_n, max_value + 1)) | \ - set(x for x in range(min_value, begin_n + 1)) - return set(x for x in range(begin_n, end_n + 1)) + return set(range(end_n, max_value + 1)) | set(range(min_value, begin_n + 1)) + return set(range(begin_n, end_n + 1)) - return {_parse_cron_int(part, special_mapping, u"Number for time expression must be an " - u"integer, got {}")} + return {_parse_cron_int(part, special_mapping, "Number for time expression must be an " + "integer, got {}")} def cron_expression_validator(name, min_value, max_value, special_mapping=None): @@ -249,7 +247,7 @@ def validate_cron_keys(value): if CONF_CRON in value: for key in value.keys(): if key in CRON_KEYS: - raise cv.Invalid("Cannot use option {} when cron: is specified.".format(key)) + raise cv.Invalid(f"Cannot use option {key} when cron: is specified.") if CONF_AT in value: raise cv.Invalid("Cannot use option at with cron!") cron_ = value[CONF_CRON] @@ -259,7 +257,7 @@ def validate_cron_keys(value): if CONF_AT in value: for key in value.keys(): if key in CRON_KEYS: - raise cv.Invalid("Cannot use option {} when at: is specified.".format(key)) + raise cv.Invalid(f"Cannot use option {key} when at: is specified.") at_ = value[CONF_AT] value = {x: value[x] for x in value if x != CONF_AT} value.update(at_) diff --git a/esphome/components/uart/__init__.py b/esphome/components/uart/__init__.py index 2511cf28b1..4312bd5d10 100644 --- a/esphome/components/uart/__init__.py +++ b/esphome/components/uart/__init__.py @@ -3,7 +3,6 @@ import esphome.config_validation as cv from esphome import pins, automation from esphome.const import CONF_BAUD_RATE, CONF_ID, CONF_RX_PIN, CONF_TX_PIN, CONF_UART_ID, CONF_DATA from esphome.core import CORE, coroutine -from esphome.py_compat import text_type, binary_type, char_to_byte uart_ns = cg.esphome_ns.namespace('uart') UARTComponent = uart_ns.class_('UARTComponent', cg.Component) @@ -13,7 +12,7 @@ MULTI_CONF = True def validate_raw_data(value): - if isinstance(value, text_type): + if isinstance(value, str): return value.encode('utf-8') if isinstance(value, str): return value @@ -77,8 +76,8 @@ def uart_write_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) yield cg.register_parented(var, config[CONF_ID]) data = config[CONF_DATA] - if isinstance(data, binary_type): - data = [char_to_byte(x) for x in data] + if isinstance(data, bytes): + data = list(data) if cg.is_template(data): templ = yield cg.templatable(data, args, cg.std_vector.template(cg.uint8)) diff --git a/esphome/components/uart/switch/__init__.py b/esphome/components/uart/switch/__init__.py index b6f622604f..6cc11d8bbe 100644 --- a/esphome/components/uart/switch/__init__.py +++ b/esphome/components/uart/switch/__init__.py @@ -3,7 +3,6 @@ import esphome.config_validation as cv from esphome.components import switch, uart from esphome.const import CONF_DATA, CONF_ID, CONF_INVERTED from esphome.core import HexInt -from esphome.py_compat import binary_type, char_to_byte from .. import uart_ns, validate_raw_data DEPENDENCIES = ['uart'] @@ -25,6 +24,6 @@ def to_code(config): yield uart.register_uart_device(var, config) data = config[CONF_DATA] - if isinstance(data, binary_type): - data = [HexInt(char_to_byte(x)) for x in data] + if isinstance(data, bytes): + data = [HexInt(x) for x in data] cg.add(var.set_data(data)) diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index 37ea20c6e4..d3c7e51603 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -30,9 +30,9 @@ def validate_password(value): if not value: return value if len(value) < 8: - raise cv.Invalid(u"WPA password must be at least 8 characters long") + raise cv.Invalid("WPA password must be at least 8 characters long") if len(value) > 64: - raise cv.Invalid(u"WPA password must be at most 64 characters long") + raise cv.Invalid("WPA password must be at most 64 characters long") return value diff --git a/esphome/config.py b/esphome/config.py index 027c28bc5d..8d7c622a27 100644 --- a/esphome/config.py +++ b/esphome/config.py @@ -1,5 +1,3 @@ -from __future__ import print_function - import collections import importlib import logging @@ -18,7 +16,6 @@ from esphome.components.substitutions import CONF_SUBSTITUTIONS from esphome.const import CONF_ESPHOME, CONF_PLATFORM, ESP_PLATFORMS from esphome.core import CORE, EsphomeError # noqa from esphome.helpers import color, indent -from esphome.py_compat import text_type, IS_PY2, decode_text, string_types from esphome.util import safe_print, OrderedDict from typing import List, Optional, Tuple, Union # noqa @@ -31,7 +28,7 @@ _LOGGER = logging.getLogger(__name__) _COMPONENT_CACHE = {} -class ComponentManifest(object): +class ComponentManifest: def __init__(self, module, base_components_path, is_core=False, is_platform=False): self.module = module self._is_core = is_core @@ -89,7 +86,7 @@ class ComponentManifest(object): source_files = core.find_source_files(os.path.join(core_p, 'dummy')) ret = {} for f in source_files: - ret['esphome/core/{}'.format(f)] = os.path.join(core_p, f) + ret[f'esphome/core/{f}'] = os.path.join(core_p, f) return ret source_files = core.find_source_files(self.module.__file__) @@ -101,7 +98,7 @@ class ComponentManifest(object): rel = os.path.relpath(full_file, self.base_components_path) # Always use / for C++ include names rel = rel.replace(os.sep, '/') - target_file = 'esphome/components/{}'.format(rel) + target_file = f'esphome/components/{rel}' ret[target_file] = full_file return ret @@ -119,12 +116,6 @@ def _mount_config_dir(): if not os.path.isdir(custom_path): CUSTOM_COMPONENTS_PATH = None return - init_path = os.path.join(custom_path, '__init__.py') - if IS_PY2 and not os.path.isfile(init_path): - _LOGGER.warning("Found 'custom_components' folder, but file __init__.py was not found. " - "ESPHome will automatically create it now....") - with open(init_path, 'w') as f: - f.write('\n') if CORE.config_dir not in sys.path: sys.path.insert(0, CORE.config_dir) CUSTOM_COMPONENTS_PATH = custom_path @@ -137,7 +128,7 @@ def _lookup_module(domain, is_platform): _mount_config_dir() # First look for custom_components try: - module = importlib.import_module('custom_components.{}'.format(domain)) + module = importlib.import_module(f'custom_components.{domain}') except ImportError as e: # ImportError when no such module if 'No module named' not in str(e): @@ -153,7 +144,7 @@ def _lookup_module(domain, is_platform): return manif try: - module = importlib.import_module('esphome.components.{}'.format(domain)) + module = importlib.import_module(f'esphome.components.{domain}') except ImportError as e: if 'No module named' not in str(e): _LOGGER.error("Unable to import component %s:", domain, exc_info=True) @@ -173,7 +164,7 @@ def get_component(domain): def get_platform(domain, platform): - full = '{}.{}'.format(platform, domain) + full = f'{platform}.{domain}' return _lookup_module(full, True) @@ -192,7 +183,7 @@ def iter_components(config): yield domain, component, conf if component.is_platform_component: for p_config in conf: - p_name = u"{}.{}".format(domain, p_config[CONF_PLATFORM]) + p_name = "{}.{}".format(domain, p_config[CONF_PLATFORM]) platform = get_platform(domain, p_config[CONF_PLATFORM]) yield p_name, platform, p_config @@ -208,13 +199,13 @@ def _path_begins_with(path, other): # type: (ConfigPath, ConfigPath) -> bool class Config(OrderedDict): def __init__(self): - super(Config, self).__init__() + super().__init__() # A list of voluptuous errors self.errors = [] # type: List[vol.Invalid] # A list of paths that should be fully outputted # The values will be the paths to all "domain", for example (['logger'], 'logger') # or (['sensor', 'ultrasonic'], 'sensor.ultrasonic') - self.output_paths = [] # type: List[Tuple[ConfigPath, unicode]] + self.output_paths = [] # type: List[Tuple[ConfigPath, str]] def add_error(self, error): # type: (vol.Invalid) -> None @@ -234,15 +225,15 @@ class Config(OrderedDict): self.add_error(e) def add_str_error(self, message, path): - # type: (basestring, ConfigPath) -> None + # type: (str, ConfigPath) -> None self.add_error(vol.Invalid(message, path)) def add_output_path(self, path, domain): - # type: (ConfigPath, unicode) -> None + # type: (ConfigPath, str) -> None self.output_paths.append((path, domain)) def remove_output_path(self, path, domain): - # type: (ConfigPath, unicode) -> None + # type: (ConfigPath, str) -> None self.output_paths.remove((path, domain)) def is_in_error_path(self, path): @@ -312,12 +303,10 @@ def iter_ids(config, path=None): yield id, path elif isinstance(config, list): for i, item in enumerate(config): - for result in iter_ids(item, path + [i]): - yield result + yield from iter_ids(item, path + [i]) elif isinstance(config, dict): for key, value in config.items(): - for result in iter_ids(value, path + [key]): - yield result + yield from iter_ids(value, path + [key]) def do_id_pass(result): # type: (Config) -> None @@ -332,8 +321,8 @@ def do_id_pass(result): # type: (Config) -> None # Look for duplicate definitions match = next((v for v in declare_ids if v[0].id == id.id), None) if match is not None: - opath = u'->'.join(text_type(v) for v in match[1]) - result.add_str_error(u"ID {} redefined! Check {}".format(id.id, opath), path) + opath = '->'.join(str(v) for v in match[1]) + result.add_str_error(f"ID {id.id} redefined! Check {opath}", path) continue declare_ids.append((id, path)) else: @@ -357,8 +346,8 @@ def do_id_pass(result): # type: (Config) -> None # Find candidates matches = difflib.get_close_matches(id.id, [v[0].id for v in declare_ids]) if matches: - matches_s = ', '.join('"{}"'.format(x) for x in matches) - error += " These IDs look similar: {}.".format(matches_s) + matches_s = ', '.join(f'"{x}"' for x in matches) + error += f" These IDs look similar: {matches_s}." result.add_str_error(error, path) continue if not isinstance(match.type, MockObjClass) or not isinstance(id.type, MockObjClass): @@ -377,7 +366,7 @@ def do_id_pass(result): # type: (Config) -> None id.id = v[0].id break else: - result.add_str_error("Couldn't resolve ID for type '{}'".format(id.type), path) + result.add_str_error(f"Couldn't resolve ID for type '{id.type}'", path) def recursive_check_replaceme(value): @@ -389,7 +378,7 @@ def recursive_check_replaceme(value): return cv.Schema({cv.valid: recursive_check_replaceme})(value) if isinstance(value, ESPForceValue): pass - if isinstance(value, string_types) and value == 'REPLACEME': + if isinstance(value, str) and value == 'REPLACEME': raise cv.Invalid("Found 'REPLACEME' in configuration, this is most likely an error. " "Please make sure you have replaced all fields from the sample " "configuration.\n" @@ -455,8 +444,8 @@ def validate_config(config): while load_queue: domain, conf = load_queue.popleft() - domain = text_type(domain) - if domain.startswith(u'.'): + domain = str(domain) + if domain.startswith('.'): # Ignore top-level keys starting with a dot continue result.add_output_path([domain], domain) @@ -464,7 +453,7 @@ def validate_config(config): component = get_component(domain) path = [domain] if component is None: - result.add_str_error(u"Component not found: {}".format(domain), path) + result.add_str_error(f"Component not found: {domain}", path) continue CORE.loaded_integrations.add(domain) @@ -492,24 +481,24 @@ def validate_config(config): for i, p_config in enumerate(conf): path = [domain, i] # Construct temporary unknown output path - p_domain = u'{}.unknown'.format(domain) + p_domain = f'{domain}.unknown' result.add_output_path(path, p_domain) result[domain][i] = p_config if not isinstance(p_config, dict): - result.add_str_error(u"Platform schemas must be key-value pairs.", path) + result.add_str_error("Platform schemas must be key-value pairs.", path) continue p_name = p_config.get('platform') if p_name is None: - result.add_str_error(u"No platform specified! See 'platform' key.", path) + result.add_str_error("No platform specified! See 'platform' key.", path) continue # Remove temp output path and construct new one result.remove_output_path(path, p_domain) - p_domain = u'{}.{}'.format(domain, p_name) + p_domain = f'{domain}.{p_name}' result.add_output_path(path, p_domain) # Try Load platform platform = get_platform(domain, p_name) if platform is None: - result.add_str_error(u"Platform not found: '{}'".format(p_domain), path) + result.add_str_error(f"Platform not found: '{p_domain}'", path) continue CORE.loaded_integrations.add(p_name) @@ -537,8 +526,8 @@ def validate_config(config): success = True for dependency in comp.dependencies: if dependency not in config: - result.add_str_error(u"Component {} requires component {}" - u"".format(domain, dependency), path) + result.add_str_error("Component {} requires component {}" + "".format(domain, dependency), path) success = False if not success: continue @@ -546,22 +535,22 @@ def validate_config(config): success = True for conflict in comp.conflicts_with: if conflict in config: - result.add_str_error(u"Component {} cannot be used together with component {}" - u"".format(domain, conflict), path) + result.add_str_error("Component {} cannot be used together with component {}" + "".format(domain, conflict), path) success = False if not success: continue if CORE.esp_platform not in comp.esp_platforms: - result.add_str_error(u"Component {} doesn't support {}.".format(domain, - CORE.esp_platform), + result.add_str_error("Component {} doesn't support {}.".format(domain, + CORE.esp_platform), path) continue if not comp.is_platform_component and comp.config_schema is None and \ not isinstance(conf, core.AutoLoad): - result.add_str_error(u"Component {} cannot be loaded via YAML " - u"(no CONFIG_SCHEMA).".format(domain), path) + result.add_str_error("Component {} cannot be loaded via YAML " + "(no CONFIG_SCHEMA).".format(domain), path) continue if comp.is_multi_conf: @@ -611,13 +600,13 @@ def _nested_getitem(data, path): def humanize_error(config, validation_error): - validation_error = text_type(validation_error) + validation_error = str(validation_error) m = re.match(r'^(.*?)\s*(?:for dictionary value )?@ data\[.*$', validation_error, re.DOTALL) if m is not None: validation_error = m.group(1) validation_error = validation_error.strip() - if not validation_error.endswith(u'.'): - validation_error += u'.' + if not validation_error.endswith('.'): + validation_error += '.' return validation_error @@ -634,22 +623,22 @@ def _get_parent_name(path, config): def _format_vol_invalid(ex, config): - # type: (vol.Invalid, Config) -> unicode - message = u'' + # type: (vol.Invalid, Config) -> str + message = '' paren = _get_parent_name(ex.path[:-1], config) if isinstance(ex, ExtraKeysInvalid): if ex.candidates: - message += u'[{}] is an invalid option for [{}]. Did you mean {}?'.format( - ex.path[-1], paren, u', '.join(u'[{}]'.format(x) for x in ex.candidates)) + message += '[{}] is an invalid option for [{}]. Did you mean {}?'.format( + ex.path[-1], paren, ', '.join(f'[{x}]' for x in ex.candidates)) else: - message += u'[{}] is an invalid option for [{}]. Please check the indentation.'.format( + message += '[{}] is an invalid option for [{}]. Please check the indentation.'.format( ex.path[-1], paren) - elif u'extra keys not allowed' in text_type(ex): - message += u'[{}] is an invalid option for [{}].'.format(ex.path[-1], paren) - elif u'required key not provided' in text_type(ex): - message += u"'{}' is a required option for [{}].".format(ex.path[-1], paren) + elif 'extra keys not allowed' in str(ex): + message += '[{}] is an invalid option for [{}].'.format(ex.path[-1], paren) + elif 'required key not provided' in str(ex): + message += "'{}' is a required option for [{}].".format(ex.path[-1], paren) else: message += humanize_error(config, ex) @@ -662,9 +651,8 @@ class InvalidYAMLError(EsphomeError): base = str(base_exc) except UnicodeDecodeError: base = repr(base_exc) - base = decode_text(base) - message = u"Invalid YAML syntax:\n\n{}".format(base) - super(InvalidYAMLError, self).__init__(message) + message = f"Invalid YAML syntax:\n\n{base}" + super().__init__(message) self.base_exc = base_exc @@ -680,7 +668,7 @@ def _load_config(): except EsphomeError: raise except Exception: - _LOGGER.error(u"Unexpected exception while reading configuration:") + _LOGGER.error("Unexpected exception while reading configuration:") raise return result @@ -690,7 +678,7 @@ def load_config(): try: return _load_config() except vol.Invalid as err: - raise EsphomeError("Error while parsing config: {}".format(err)) + raise EsphomeError(f"Error while parsing config: {err}") def line_info(obj, highlight=True): @@ -699,7 +687,7 @@ def line_info(obj, highlight=True): return None if isinstance(obj, ESPHomeDataBase) and obj.esp_range is not None: mark = obj.esp_range.start_mark - source = u"[source {}:{}]".format(mark.document, mark.line + 1) + source = "[source {}:{}]".format(mark.document, mark.line + 1) return color('cyan', source) return None @@ -715,82 +703,82 @@ def _print_on_next_line(obj): def dump_dict(config, path, at_root=True): - # type: (Config, ConfigPath, bool) -> Tuple[unicode, bool] + # type: (Config, ConfigPath, bool) -> Tuple[str, bool] conf = config.get_nested_item(path) - ret = u'' + ret = '' multiline = False if at_root: error = config.get_error_for_path(path) if error is not None: - ret += u'\n' + color('bold_red', _format_vol_invalid(error, config)) + u'\n' + ret += '\n' + color('bold_red', _format_vol_invalid(error, config)) + '\n' if isinstance(conf, (list, tuple)): multiline = True if not conf: - ret += u'[]' + ret += '[]' multiline = False for i in range(len(conf)): path_ = path + [i] error = config.get_error_for_path(path_) if error is not None: - ret += u'\n' + color('bold_red', _format_vol_invalid(error, config)) + u'\n' + ret += '\n' + color('bold_red', _format_vol_invalid(error, config)) + '\n' - sep = u'- ' + sep = '- ' if config.is_in_error_path(path_): sep = color('red', sep) msg, _ = dump_dict(config, path_, at_root=False) msg = indent(msg) inf = line_info(config.get_nested_item(path_), highlight=config.is_in_error_path(path_)) if inf is not None: - msg = inf + u'\n' + msg + msg = inf + '\n' + msg elif msg: msg = msg[2:] - ret += sep + msg + u'\n' + ret += sep + msg + '\n' elif isinstance(conf, dict): multiline = True if not conf: - ret += u'{}' + ret += '{}' multiline = False for k in conf.keys(): path_ = path + [k] error = config.get_error_for_path(path_) if error is not None: - ret += u'\n' + color('bold_red', _format_vol_invalid(error, config)) + u'\n' + ret += '\n' + color('bold_red', _format_vol_invalid(error, config)) + '\n' - st = u'{}: '.format(k) + st = f'{k}: ' if config.is_in_error_path(path_): st = color('red', st) msg, m = dump_dict(config, path_, at_root=False) inf = line_info(config.get_nested_item(path_), highlight=config.is_in_error_path(path_)) if m: - msg = u'\n' + indent(msg) + msg = '\n' + indent(msg) if inf is not None: if m: - msg = u' ' + inf + msg + msg = ' ' + inf + msg else: - msg = msg + u' ' + inf - ret += st + msg + u'\n' + msg = msg + ' ' + inf + ret += st + msg + '\n' elif isinstance(conf, str): if is_secret(conf): - conf = u'!secret {}'.format(is_secret(conf)) + conf = '!secret {}'.format(is_secret(conf)) if not conf: - conf += u"''" + conf += "''" if len(conf) > 80: - conf = u'|-\n' + indent(conf) + conf = '|-\n' + indent(conf) error = config.get_error_for_path(path) col = 'bold_red' if error else 'white' - ret += color(col, text_type(conf)) + ret += color(col, str(conf)) elif isinstance(conf, core.Lambda): if is_secret(conf): - conf = u'!secret {}'.format(is_secret(conf)) + conf = '!secret {}'.format(is_secret(conf)) - conf = u'!lambda |-\n' + indent(text_type(conf.value)) + conf = '!lambda |-\n' + indent(str(conf.value)) error = config.get_error_for_path(path) col = 'bold_red' if error else 'white' ret += color(col, conf) @@ -799,8 +787,8 @@ def dump_dict(config, path, at_root=True): else: error = config.get_error_for_path(path) col = 'bold_red' if error else 'white' - ret += color(col, text_type(conf)) - multiline = u'\n' in ret + ret += color(col, str(conf)) + multiline = '\n' in ret return ret, multiline @@ -830,20 +818,20 @@ def read_config(): try: res = load_config() except EsphomeError as err: - _LOGGER.error(u"Error while reading config: %s", err) + _LOGGER.error("Error while reading config: %s", err) return None if res.errors: if not CORE.verbose: res = strip_default_ids(res) - safe_print(color('bold_red', u"Failed config")) + safe_print(color('bold_red', "Failed config")) safe_print('') for path, domain in res.output_paths: if not res.is_in_error_path(path): continue - safe_print(color('bold_red', u'{}:'.format(domain)) + u' ' + - (line_info(res.get_nested_item(path)) or u'')) + safe_print(color('bold_red', f'{domain}:') + ' ' + + (line_info(res.get_nested_item(path)) or '')) safe_print(indent(dump_dict(res, path)[0])) return None return OrderedDict(res) diff --git a/esphome/config_helpers.py b/esphome/config_helpers.py index 0c508d2202..dcbcb70efe 100644 --- a/esphome/config_helpers.py +++ b/esphome/config_helpers.py @@ -1,22 +1,19 @@ -from __future__ import print_function - import json import os from esphome.core import CORE from esphome.helpers import read_file -from esphome.py_compat import safe_input def read_config_file(path): - # type: (basestring) -> unicode + # type: (str) -> str if CORE.vscode and (not CORE.ace or os.path.abspath(path) == os.path.abspath(CORE.config_path)): print(json.dumps({ 'type': 'read_file', 'path': path, })) - data = json.loads(safe_input()) + data = json.loads(input()) assert data['type'] == 'file_response' return data['content'] diff --git a/esphome/config_validation.py b/esphome/config_validation.py index 88fb55e841..55199e6647 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -1,6 +1,4 @@ -# coding=utf-8 """Helpers for config validation using voluptuous.""" -from __future__ import print_function import logging import os @@ -20,7 +18,6 @@ from esphome.const import CONF_AVAILABILITY, CONF_COMMAND_TOPIC, CONF_DISCOVERY, from esphome.core import CORE, HexInt, IPAddress, Lambda, TimePeriod, TimePeriodMicroseconds, \ TimePeriodMilliseconds, TimePeriodSeconds, TimePeriodMinutes from esphome.helpers import list_starts_with, add_class_to_obj -from esphome.py_compat import integer_types, string_types, text_type, IS_PY2, decode_text from esphome.voluptuous_schema import _Schema _LOGGER = logging.getLogger(__name__) @@ -43,7 +40,7 @@ ALLOW_EXTRA = vol.ALLOW_EXTRA UNDEFINED = vol.UNDEFINED RequiredFieldInvalid = vol.RequiredFieldInvalid -ALLOWED_NAME_CHARS = u'abcdefghijklmnopqrstuvwxyz0123456789_' +ALLOWED_NAME_CHARS = 'abcdefghijklmnopqrstuvwxyz0123456789_' RESERVED_IDS = [ # C++ keywords http://en.cppreference.com/w/cpp/keyword @@ -82,7 +79,7 @@ class Optional(vol.Optional): """ def __init__(self, key, default=UNDEFINED): - super(Optional, self).__init__(key, default=default) + super().__init__(key, default=default) class Required(vol.Required): @@ -94,7 +91,7 @@ class Required(vol.Required): """ def __init__(self, key): - super(Required, self).__init__(key) + super().__init__(key) def check_not_templatable(value): @@ -105,7 +102,7 @@ def check_not_templatable(value): def alphanumeric(value): if value is None: raise Invalid("string value is None") - value = text_type(value) + value = str(value) if not value.isalnum(): raise Invalid("string value is not alphanumeric") return value @@ -115,8 +112,8 @@ def valid_name(value): value = string_strict(value) for c in value: if c not in ALLOWED_NAME_CHARS: - raise Invalid(u"'{}' is an invalid character for names. Valid characters are: {}" - u" (lowercase, no spaces)".format(c, ALLOWED_NAME_CHARS)) + raise Invalid(f"'{c}' is an invalid character for names. Valid characters are: " + f"{ALLOWED_NAME_CHARS} (lowercase, no spaces)") return value @@ -131,10 +128,10 @@ def string(value): raise Invalid("string value cannot be dictionary or list.") if isinstance(value, bool): raise Invalid("Auto-converted this value to boolean, please wrap the value in quotes.") - if isinstance(value, text_type): + if isinstance(value, str): return value if value is not None: - return text_type(value) + return str(value) raise Invalid("string value is None") @@ -142,10 +139,8 @@ def string_strict(value): """Like string, but only allows strings, and does not automatically convert other types to strings.""" check_not_templatable(value) - if isinstance(value, text_type): + if isinstance(value, str): return value - if isinstance(value, string_types): - return text_type(value) raise Invalid("Must be string, got {}. did you forget putting quotes " "around the value?".format(type(value))) @@ -172,14 +167,14 @@ def boolean(value): check_not_templatable(value) if isinstance(value, bool): return value - if isinstance(value, string_types): + if isinstance(value, str): value = value.lower() if value in ('true', 'yes', 'on', 'enable'): return True if value in ('false', 'no', 'off', 'disable'): return False - raise Invalid(u"Expected boolean value, but cannot convert {} to a boolean. " - u"Please use 'true' or 'false'".format(value)) + raise Invalid("Expected boolean value, but cannot convert {} to a boolean. " + "Please use 'true' or 'false'".format(value)) def ensure_list(*validators): @@ -228,7 +223,7 @@ def int_(value): Automatically also converts strings to ints. """ check_not_templatable(value) - if isinstance(value, integer_types): + if isinstance(value, int): return value if isinstance(value, float): if int(value) == value: @@ -242,15 +237,15 @@ def int_(value): try: return int(value, base) except ValueError: - raise Invalid(u"Expected integer, but cannot parse {} as an integer".format(value)) + raise Invalid(f"Expected integer, but cannot parse {value} as an integer") def int_range(min=None, max=None, min_included=True, max_included=True): """Validate that the config option is an integer in the given range.""" if min is not None: - assert isinstance(min, integer_types) + assert isinstance(min, int) if max is not None: - assert isinstance(max, integer_types) + assert isinstance(max, int) return All(int_, Range(min=min, max=max, min_included=min_included, max_included=max_included)) @@ -291,14 +286,14 @@ def validate_id_name(value): valid_chars = ascii_letters + digits + '_' for char in value: if char not in valid_chars: - raise Invalid(u"IDs must only consist of upper/lowercase characters, the underscore" - u"character and numbers. The character '{}' cannot be used" - u"".format(char)) + raise Invalid("IDs must only consist of upper/lowercase characters, the underscore" + "character and numbers. The character '{}' cannot be used" + "".format(char)) if value in RESERVED_IDS: - raise Invalid(u"ID '{}' is reserved internally and cannot be used".format(value)) + raise Invalid(f"ID '{value}' is reserved internally and cannot be used") if value in CORE.loaded_integrations: - raise Invalid(u"ID '{}' conflicts with the name of an esphome integration, please use " - u"another ID name.".format(value)) + raise Invalid("ID '{}' conflicts with the name of an esphome integration, please use " + "another ID name.".format(value)) return value @@ -358,7 +353,7 @@ def only_on(platforms): def validator_(obj): if CORE.esp_platform not in platforms: - raise Invalid(u"This feature is only available on {}".format(platforms)) + raise Invalid(f"This feature is only available on {platforms}") return obj return validator_ @@ -463,7 +458,7 @@ def time_period_str_unit(value): "'{0}s'?".format(value)) if isinstance(value, TimePeriod): value = str(value) - if not isinstance(value, string_types): + if not isinstance(value, str): raise Invalid("Expected string for time period with unit.") unit_to_kwarg = { @@ -485,8 +480,8 @@ def time_period_str_unit(value): match = re.match(r"^([-+]?[0-9]*\.?[0-9]*)\s*(\w*)$", value) if match is None: - raise Invalid(u"Expected time period with unit, " - u"got {}".format(value)) + raise Invalid("Expected time period with unit, " + "got {}".format(value)) kwarg = unit_to_kwarg[one_of(*unit_to_kwarg)(match.group(2))] return TimePeriod(**{kwarg: float(match.group(1))}) @@ -545,7 +540,7 @@ def time_of_day(value): try: date = datetime.strptime(value, '%H:%M:%S %p') except ValueError: - raise Invalid("Invalid time of day: {}".format(err)) + raise Invalid(f"Invalid time of day: {err}") return { CONF_HOUR: date.hour, @@ -577,7 +572,7 @@ def uuid(value): METRIC_SUFFIXES = { 'E': 1e18, 'P': 1e15, 'T': 1e12, 'G': 1e9, 'M': 1e6, 'k': 1e3, 'da': 10, 'd': 1e-1, - 'c': 1e-2, 'm': 0.001, u'µ': 1e-6, 'u': 1e-6, 'n': 1e-9, 'p': 1e-12, 'f': 1e-15, 'a': 1e-18, + 'c': 1e-2, 'm': 0.001, 'µ': 1e-6, 'u': 1e-6, 'n': 1e-9, 'p': 1e-12, 'f': 1e-15, 'a': 1e-18, '': 1 } @@ -594,11 +589,11 @@ def float_with_unit(quantity, regex_suffix, optional_unit=False): match = pattern.match(string(value)) if match is None: - raise Invalid(u"Expected {} with unit, got {}".format(quantity, value)) + raise Invalid(f"Expected {quantity} with unit, got {value}") mantissa = float(match.group(1)) if match.group(2) not in METRIC_SUFFIXES: - raise Invalid(u"Invalid {} suffix {}".format(quantity, match.group(2))) + raise Invalid("Invalid {} suffix {}".format(quantity, match.group(2))) multiplier = METRIC_SUFFIXES[match.group(2)] return mantissa * multiplier @@ -606,30 +601,17 @@ def float_with_unit(quantity, regex_suffix, optional_unit=False): return validator -frequency = float_with_unit("frequency", u"(Hz|HZ|hz)?") -resistance = float_with_unit("resistance", u"(Ω|Ω|ohm|Ohm|OHM)?") -current = float_with_unit("current", u"(a|A|amp|Amp|amps|Amps|ampere|Ampere)?") -voltage = float_with_unit("voltage", u"(v|V|volt|Volts)?") -distance = float_with_unit("distance", u"(m)") -framerate = float_with_unit("framerate", u"(FPS|fps|Fps|FpS|Hz)") -angle = float_with_unit("angle", u"(°|deg)", optional_unit=True) -_temperature_c = float_with_unit("temperature", u"(°C|° C|°|C)?") -_temperature_k = float_with_unit("temperature", u"(° K|° K|K)?") -_temperature_f = float_with_unit("temperature", u"(°F|° F|F)?") -decibel = float_with_unit("decibel", u"(dB|dBm|db|dbm)", optional_unit=True) - -if IS_PY2: - # Override voluptuous invalid to unicode for py2 - def _vol_invalid_unicode(self): - path = u' @ data[%s]' % u']['.join(map(repr, self.path)) \ - if self.path else u'' - # pylint: disable=no-member - output = decode_text(self.message) - if self.error_type: - output += u' for ' + self.error_type - return output + path - - Invalid.__unicode__ = _vol_invalid_unicode +frequency = float_with_unit("frequency", "(Hz|HZ|hz)?") +resistance = float_with_unit("resistance", "(Ω|Ω|ohm|Ohm|OHM)?") +current = float_with_unit("current", "(a|A|amp|Amp|amps|Amps|ampere|Ampere)?") +voltage = float_with_unit("voltage", "(v|V|volt|Volts)?") +distance = float_with_unit("distance", "(m)") +framerate = float_with_unit("framerate", "(FPS|fps|Fps|FpS|Hz)") +angle = float_with_unit("angle", "(°|deg)", optional_unit=True) +_temperature_c = float_with_unit("temperature", "(°C|° C|°|C)?") +_temperature_k = float_with_unit("temperature", "(° K|° K|K)?") +_temperature_f = float_with_unit("temperature", "(°F|° F|F)?") +decibel = float_with_unit("decibel", "(dB|dBm|db|dbm)", optional_unit=True) def temperature(value): @@ -672,15 +654,15 @@ def validate_bytes(value): match = re.match(r"^([0-9]+)\s*(\w*?)(?:byte|B|b)?s?$", value) if match is None: - raise Invalid(u"Expected number of bytes with unit, got {}".format(value)) + raise Invalid(f"Expected number of bytes with unit, got {value}") mantissa = int(match.group(1)) if match.group(2) not in METRIC_SUFFIXES: - raise Invalid(u"Invalid metric suffix {}".format(match.group(2))) + raise Invalid("Invalid metric suffix {}".format(match.group(2))) multiplier = METRIC_SUFFIXES[match.group(2)] if multiplier < 1: - raise Invalid(u"Only suffixes with positive exponents are supported. " - u"Got {}".format(match.group(2))) + raise Invalid("Only suffixes with positive exponents are supported. " + "Got {}".format(match.group(2))) return int(mantissa * multiplier) @@ -701,7 +683,7 @@ def domain(value): try: return str(ipv4(value)) except Invalid: - raise Invalid("Invalid domain: {}".format(value)) + raise Invalid(f"Invalid domain: {value}") def domain_name(value): @@ -730,7 +712,7 @@ def ssid(value): def ipv4(value): if isinstance(value, list): parts = value - elif isinstance(value, string_types): + elif isinstance(value, str): parts = value.split('.') elif isinstance(value, IPAddress): return value @@ -806,7 +788,7 @@ def mqtt_qos(value): try: value = int(value) except (TypeError, ValueError): - raise Invalid(u"MQTT Quality of Service must be integer, got {}".format(value)) + raise Invalid(f"MQTT Quality of Service must be integer, got {value}") return one_of(0, 1, 2)(value) @@ -814,7 +796,7 @@ def requires_component(comp): """Validate that this option can only be specified when the component `comp` is loaded.""" def validator(value): if comp not in CORE.raw_config: - raise Invalid("This option requires component {}".format(comp)) + raise Invalid(f"This option requires component {comp}") return value return validator @@ -839,7 +821,7 @@ def percentage(value): def possibly_negative_percentage(value): - has_percent_sign = isinstance(value, string_types) and value.endswith('%') + has_percent_sign = isinstance(value, str) and value.endswith('%') if has_percent_sign: value = float(value[:-1].rstrip()) / 100.0 if value > 1: @@ -856,7 +838,7 @@ def possibly_negative_percentage(value): def percentage_int(value): - if isinstance(value, string_types) and value.endswith('%'): + if isinstance(value, str) and value.endswith('%'): value = int(value[:-1].rstrip()) return value @@ -916,7 +898,7 @@ def one_of(*values, **kwargs): - *float* (``bool``, default=False): Whether to convert the incoming values to floats. - *space* (``str``, default=' '): What to convert spaces in the input string to. """ - options = u', '.join(u"'{}'".format(x) for x in values) + options = ', '.join(f"'{x}'" for x in values) lower = kwargs.pop('lower', False) upper = kwargs.pop('upper', False) string_ = kwargs.pop('string', False) or lower or upper @@ -940,13 +922,13 @@ def one_of(*values, **kwargs): value = Upper(value) if value not in values: import difflib - options_ = [text_type(x) for x in values] - option = text_type(value) + options_ = [str(x) for x in values] + option = str(value) matches = difflib.get_close_matches(option, options_) if matches: - raise Invalid(u"Unknown value '{}', did you mean {}?" - u"".format(value, u", ".join(u"'{}'".format(x) for x in matches))) - raise Invalid(u"Unknown value '{}', valid options are {}.".format(value, options)) + raise Invalid("Unknown value '{}', did you mean {}?" + "".format(value, ", ".join(f"'{x}'" for x in matches))) + raise Invalid(f"Unknown value '{value}', valid options are {options}.") return value return validator @@ -996,7 +978,7 @@ def returning_lambda(value): Additionally, make sure the lambda returns something. """ value = lambda_(value) - if u'return' not in value.value: + if 'return' not in value.value: raise Invalid("Lambda doesn't contain a 'return' statement, but the lambda " "is expected to return a value. \n" "Please make sure the lambda contains at least one " @@ -1007,24 +989,23 @@ def returning_lambda(value): def dimensions(value): if isinstance(value, list): if len(value) != 2: - raise Invalid(u"Dimensions must have a length of two, not {}".format(len(value))) + raise Invalid("Dimensions must have a length of two, not {}".format(len(value))) try: width, height = int(value[0]), int(value[1]) except ValueError: - raise Invalid(u"Width and height dimensions must be integers") + raise Invalid("Width and height dimensions must be integers") if width <= 0 or height <= 0: - raise Invalid(u"Width and height must at least be 1") + raise Invalid("Width and height must at least be 1") return [width, height] value = string(value) match = re.match(r"\s*([0-9]+)\s*[xX]\s*([0-9]+)\s*", value) if not match: - raise Invalid(u"Invalid value '{}' for dimensions. Only WIDTHxHEIGHT is allowed.") + raise Invalid("Invalid value '{}' for dimensions. Only WIDTHxHEIGHT is allowed.") return dimensions([match.group(1), match.group(2)]) def directory(value): import json - from esphome.py_compat import safe_input value = string(value) path = CORE.relative_config_path(value) @@ -1034,25 +1015,24 @@ def directory(value): 'type': 'check_directory_exists', 'path': path, })) - data = json.loads(safe_input()) + data = json.loads(input()) assert data['type'] == 'directory_exists_response' if data['content']: return value - raise Invalid(u"Could not find directory '{}'. Please make sure it exists (full path: {})." - u"".format(path, os.path.abspath(path))) + raise Invalid("Could not find directory '{}'. Please make sure it exists (full path: {})." + "".format(path, os.path.abspath(path))) if not os.path.exists(path): - raise Invalid(u"Could not find directory '{}'. Please make sure it exists (full path: {})." - u"".format(path, os.path.abspath(path))) + raise Invalid("Could not find directory '{}'. Please make sure it exists (full path: {})." + "".format(path, os.path.abspath(path))) if not os.path.isdir(path): - raise Invalid(u"Path '{}' is not a directory (full path: {})." - u"".format(path, os.path.abspath(path))) + raise Invalid("Path '{}' is not a directory (full path: {})." + "".format(path, os.path.abspath(path))) return value def file_(value): import json - from esphome.py_compat import safe_input value = string(value) path = CORE.relative_config_path(value) @@ -1062,19 +1042,19 @@ def file_(value): 'type': 'check_file_exists', 'path': path, })) - data = json.loads(safe_input()) + data = json.loads(input()) assert data['type'] == 'file_exists_response' if data['content']: return value - raise Invalid(u"Could not find file '{}'. Please make sure it exists (full path: {})." - u"".format(path, os.path.abspath(path))) + raise Invalid("Could not find file '{}'. Please make sure it exists (full path: {})." + "".format(path, os.path.abspath(path))) if not os.path.exists(path): - raise Invalid(u"Could not find file '{}'. Please make sure it exists (full path: {})." - u"".format(path, os.path.abspath(path))) + raise Invalid("Could not find file '{}'. Please make sure it exists (full path: {})." + "".format(path, os.path.abspath(path))) if not os.path.isfile(path): - raise Invalid(u"Path '{}' is not a file (full path: {})." - u"".format(path, os.path.abspath(path))) + raise Invalid("Path '{}' is not a file (full path: {})." + "".format(path, os.path.abspath(path))) return value @@ -1092,7 +1072,7 @@ def entity_id(value): for x in value.split('.'): for c in x: if c not in ENTITY_ID_CHARACTERS: - raise Invalid("Invalid character for entity ID: {}".format(c)) + raise Invalid(f"Invalid character for entity ID: {c}") return value @@ -1103,9 +1083,9 @@ def extract_keys(schema): assert isinstance(schema, dict) keys = [] for skey in list(schema.keys()): - if isinstance(skey, string_types): + if isinstance(skey, str): keys.append(skey) - elif isinstance(skey, vol.Marker) and isinstance(skey.schema, string_types): + elif isinstance(skey, vol.Marker) and isinstance(skey.schema, str): keys.append(skey.schema) else: raise ValueError() @@ -1136,14 +1116,14 @@ class GenerateID(Optional): """Mark this key as being an auto-generated ID key.""" def __init__(self, key=CONF_ID): - super(GenerateID, self).__init__(key, default=lambda: None) + super().__init__(key, default=lambda: None) class SplitDefault(Optional): """Mark this key to have a split default for ESP8266/ESP32.""" def __init__(self, key, esp8266=vol.UNDEFINED, esp32=vol.UNDEFINED): - super(SplitDefault, self).__init__(key) + super().__init__(key) self._esp8266_default = vol.default_factory(esp8266) self._esp32_default = vol.default_factory(esp32) @@ -1165,7 +1145,7 @@ class OnlyWith(Optional): """Set the default value only if the given component is loaded.""" def __init__(self, key, component, default=None): - super(OnlyWith, self).__init__(key) + super().__init__(key) self._component = component self._default = vol.default_factory(default) @@ -1207,21 +1187,21 @@ def validate_registry_entry(name, registry): ignore_keys = extract_keys(base_schema) def validator(value): - if isinstance(value, string_types): + if isinstance(value, str): value = {value: {}} if not isinstance(value, dict): - raise Invalid(u"{} must consist of key-value mapping! Got {}" - u"".format(name.title(), value)) + raise Invalid("{} must consist of key-value mapping! Got {}" + "".format(name.title(), value)) key = next((x for x in value if x not in ignore_keys), None) if key is None: - raise Invalid(u"Key missing from {}! Got {}".format(name, value)) + raise Invalid(f"Key missing from {name}! Got {value}") if key not in registry: - raise Invalid(u"Unable to find {} with the name '{}'".format(name, key), [key]) + raise Invalid(f"Unable to find {name} with the name '{key}'", [key]) key2 = next((x for x in value if x != key and x not in ignore_keys), None) if key2 is not None: - raise Invalid(u"Cannot have two {0}s in one item. Key '{1}' overrides '{2}'! " - u"Did you forget to indent the block inside the {0}?" - u"".format(name, key, key2)) + raise Invalid("Cannot have two {0}s in one item. Key '{1}' overrides '{2}'! " + "Did you forget to indent the block inside the {0}?" + "".format(name, key, key2)) if value[key] is None: value[key] = {} @@ -1296,7 +1276,7 @@ def polling_component_schema(default_update_interval): return COMPONENT_SCHEMA.extend({ Required(CONF_UPDATE_INTERVAL): default_update_interval, }) - assert isinstance(default_update_interval, string_types) + assert isinstance(default_update_interval, str) return COMPONENT_SCHEMA.extend({ Optional(CONF_UPDATE_INTERVAL, default=default_update_interval): update_interval, }) diff --git a/esphome/const.py b/esphome/const.py index 0790a49552..8a45f27880 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,17 +1,16 @@ -# coding=utf-8 """Constants used by esphome.""" MAJOR_VERSION = 1 MINOR_VERSION = 15 PATCH_VERSION = '0-dev' -__short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) -__version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) +__short_version__ = f'{MAJOR_VERSION}.{MINOR_VERSION}' +__version__ = f'{__short_version__}.{PATCH_VERSION}' ESP_PLATFORM_ESP32 = 'ESP32' ESP_PLATFORM_ESP8266 = 'ESP8266' ESP_PLATFORMS = [ESP_PLATFORM_ESP32, ESP_PLATFORM_ESP8266] -ALLOWED_NAME_CHARS = u'abcdefghijklmnopqrstuvwxyz0123456789_' +ALLOWED_NAME_CHARS = 'abcdefghijklmnopqrstuvwxyz0123456789_' ARDUINO_VERSION_ESP32_DEV = 'https://github.com/platformio/platform-espressif32.git#feature/stage' ARDUINO_VERSION_ESP32_1_0_0 = 'espressif32@1.5.0' ARDUINO_VERSION_ESP32_1_0_1 = 'espressif32@1.6.0' @@ -544,12 +543,12 @@ ICON_WEATHER_WINDY = 'mdi:weather-windy' ICON_WIFI = 'mdi:wifi' UNIT_AMPERE = 'A' -UNIT_CELSIUS = u'°C' -UNIT_COUNTS_PER_CUBIC_METER = u'#/m³' +UNIT_CELSIUS = '°C' +UNIT_COUNTS_PER_CUBIC_METER = '#/m³' UNIT_DECIBEL = 'dB' UNIT_DECIBEL_MILLIWATT = 'dBm' -UNIT_DEGREE_PER_SECOND = u'°/s' -UNIT_DEGREES = u'°' +UNIT_DEGREE_PER_SECOND = '°/s' +UNIT_DEGREES = '°' UNIT_EMPTY = '' UNIT_G = 'G' UNIT_HECTOPASCAL = 'hPa' @@ -559,12 +558,12 @@ UNIT_KILOMETER = 'km' UNIT_KILOMETER_PER_HOUR = 'km/h' UNIT_LUX = 'lx' UNIT_METER = 'm' -UNIT_METER_PER_SECOND_SQUARED = u'm/s²' -UNIT_MICROGRAMS_PER_CUBIC_METER = u'µg/m³' +UNIT_METER_PER_SECOND_SQUARED = 'm/s²' +UNIT_MICROGRAMS_PER_CUBIC_METER = 'µg/m³' UNIT_MICROMETER = 'µm' -UNIT_MICROSIEMENS_PER_CENTIMETER = u'µS/cm' -UNIT_MICROTESLA = u'µT' -UNIT_OHM = u'Ω' +UNIT_MICROSIEMENS_PER_CENTIMETER = 'µS/cm' +UNIT_MICROTESLA = 'µT' +UNIT_OHM = 'Ω' UNIT_PARTS_PER_BILLION = 'ppb' UNIT_PARTS_PER_MILLION = 'ppm' UNIT_PERCENT = '%' diff --git a/esphome/core.py b/esphome/core.py index 9df30384bc..96447e560d 100644 --- a/esphome/core.py +++ b/esphome/core.py @@ -13,7 +13,6 @@ from typing import Any, Dict, List, Optional, Set # noqa from esphome.const import CONF_ARDUINO_VERSION, SOURCE_FILE_EXTENSIONS, \ CONF_COMMENT, CONF_ESPHOME, CONF_USE_ADDRESS, CONF_WIFI from esphome.helpers import ensure_unique_string, is_hassio -from esphome.py_compat import IS_PY2, integer_types, text_type, string_types from esphome.util import OrderedDict _LOGGER = logging.getLogger(__name__) @@ -23,53 +22,47 @@ class EsphomeError(Exception): """General ESPHome exception occurred.""" -if IS_PY2: - base_int = long -else: - base_int = int - - -class HexInt(base_int): +class HexInt(int): def __str__(self): if 0 <= self <= 255: - return "0x{:02X}".format(self) - return "0x{:X}".format(self) + return f"0x{self:02X}" + return f"0x{self:X}" -class IPAddress(object): +class IPAddress: def __init__(self, *args): if len(args) != 4: - raise ValueError(u"IPAddress must consist up 4 items") + raise ValueError("IPAddress must consist up 4 items") self.args = args def __str__(self): return '.'.join(str(x) for x in self.args) -class MACAddress(object): +class MACAddress: def __init__(self, *parts): if len(parts) != 6: - raise ValueError(u"MAC Address must consist of 6 items") + raise ValueError("MAC Address must consist of 6 items") self.parts = parts def __str__(self): - return ':'.join('{:02X}'.format(part) for part in self.parts) + return ':'.join(f'{part:02X}' for part in self.parts) @property def as_hex(self): from esphome.cpp_generator import RawExpression - num = ''.join('{:02X}'.format(part) for part in self.parts) - return RawExpression('0x{}ULL'.format(num)) + num = ''.join(f'{part:02X}' for part in self.parts) + return RawExpression(f'0x{num}ULL') def is_approximately_integer(value): - if isinstance(value, integer_types): + if isinstance(value, int): return True return abs(value - round(value)) < 0.001 -class TimePeriod(object): +class TimePeriod: def __init__(self, microseconds=None, milliseconds=None, seconds=None, minutes=None, hours=None, days=None): if days is not None: @@ -137,17 +130,17 @@ class TimePeriod(object): def __str__(self): if self.microseconds is not None: - return '{}us'.format(self.total_microseconds) + return f'{self.total_microseconds}us' if self.milliseconds is not None: - return '{}ms'.format(self.total_milliseconds) + return f'{self.total_milliseconds}ms' if self.seconds is not None: - return '{}s'.format(self.total_seconds) + return f'{self.total_seconds}s' if self.minutes is not None: - return '{}min'.format(self.total_minutes) + return f'{self.total_minutes}min' if self.hours is not None: - return '{}h'.format(self.total_hours) + return f'{self.total_hours}h' if self.days is not None: - return '{}d'.format(self.total_days) + return f'{self.total_days}d' return '0s' @property @@ -224,7 +217,7 @@ class TimePeriodMinutes(TimePeriod): LAMBDA_PROG = re.compile(r'id\(\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\)(\.?)') -class Lambda(object): +class Lambda: def __init__(self, value): # pylint: disable=protected-access if isinstance(value, Lambda): @@ -260,10 +253,10 @@ class Lambda(object): return self.value def __repr__(self): - return u'Lambda<{}>'.format(self.value) + return f'Lambda<{self.value}>' -class ID(object): +class ID: def __init__(self, id, is_declaration=False, type=None, is_manual=None): self.id = id if is_manual is None: @@ -289,7 +282,7 @@ class ID(object): return self.id def __repr__(self): - return u'ID<{} declaration={}, type={}, manual={}>'.format( + return 'ID<{} declaration={}, type={}, manual={}>'.format( self.id, self.is_declaration, self.type, self.is_manual) def __eq__(self, other): @@ -305,10 +298,10 @@ class ID(object): is_manual=self.is_manual) -class DocumentLocation(object): +class DocumentLocation: def __init__(self, document, line, column): - # type: (basestring, int, int) -> None - self.document = document # type: basestring + # type: (str, int, int) -> None + self.document = document # type: str self.line = line # type: int self.column = column # type: int @@ -321,10 +314,10 @@ class DocumentLocation(object): ) def __str__(self): - return u'{} {}:{}'.format(self.document, self.line, self.column) + return f'{self.document} {self.line}:{self.column}' -class DocumentRange(object): +class DocumentRange: def __init__(self, start_mark, end_mark): # type: (DocumentLocation, DocumentLocation) -> None self.start_mark = start_mark # type: DocumentLocation @@ -338,10 +331,10 @@ class DocumentRange(object): ) def __str__(self): - return u'[{} - {}]'.format(self.start_mark, self.end_mark) + return f'[{self.start_mark} - {self.end_mark}]' -class Define(object): +class Define: def __init__(self, name, value=None): self.name = name self.value = value @@ -349,14 +342,14 @@ class Define(object): @property def as_build_flag(self): if self.value is None: - return u'-D{}'.format(self.name) - return u'-D{}={}'.format(self.name, self.value) + return f'-D{self.name}' + return f'-D{self.name}={self.value}' @property def as_macro(self): if self.value is None: - return u'#define {}'.format(self.name) - return u'#define {} {}'.format(self.name, self.value) + return f'#define {self.name}' + return f'#define {self.name} {self.value}' @property def as_tuple(self): @@ -369,7 +362,7 @@ class Define(object): return isinstance(self, type(other)) and self.as_tuple == other.as_tuple -class Library(object): +class Library: def __init__(self, name, version): self.name = name self.version = version @@ -378,7 +371,7 @@ class Library(object): def as_lib_dep(self): if self.version is None: return self.name - return u'{}@{}'.format(self.name, self.version) + return f'{self.name}@{self.version}' @property def as_tuple(self): @@ -461,7 +454,7 @@ def find_source_files(file): # pylint: disable=too-many-instance-attributes,too-many-public-methods -class EsphomeCore(object): +class EsphomeCore: def __init__(self): # True if command is run from dashboard self.dashboard = False @@ -499,7 +492,7 @@ class EsphomeCore(object): # A set of build flags to set in the platformio project self.build_flags = set() # type: Set[str] # A set of defines to set for the compile process in esphome/core/defines.h - self.defines = set() # type: Set[Define] + self.defines = set() # type: Set['Define'] # A dictionary of started coroutines, used to warn when a coroutine was not # awaited. self.active_coroutines = {} # type: Dict[int, Any] @@ -634,15 +627,15 @@ class EsphomeCore(object): # Print not-awaited coroutines for obj in self.active_coroutines.values(): - _LOGGER.warning(u"Coroutine '%s' %s was never awaited with 'yield'.", obj.__name__, obj) - _LOGGER.warning(u"Please file a bug report with your configuration.") + _LOGGER.warning("Coroutine '%s' %s was never awaited with 'yield'.", obj.__name__, obj) + _LOGGER.warning("Please file a bug report with your configuration.") if self.active_coroutines: raise EsphomeError() if self.component_ids: - comps = u', '.join(u"'{}'".format(x) for x in self.component_ids) - _LOGGER.warning(u"Components %s were never registered. Please create a bug report", + comps = ', '.join(f"'{x}'" for x in self.component_ids) + _LOGGER.warning("Components %s were never registered. Please create a bug report", comps) - _LOGGER.warning(u"with your configuration.") + _LOGGER.warning("with your configuration.") raise EsphomeError() self.active_coroutines.clear() @@ -652,8 +645,8 @@ class EsphomeCore(object): if isinstance(expression, Expression): expression = statement(expression) if not isinstance(expression, Statement): - raise ValueError(u"Add '{}' must be expression or statement, not {}" - u"".format(expression, type(expression))) + raise ValueError("Add '{}' must be expression or statement, not {}" + "".format(expression, type(expression))) self.main_statements.append(expression) _LOGGER.debug("Adding: %s", expression) @@ -665,16 +658,16 @@ class EsphomeCore(object): if isinstance(expression, Expression): expression = statement(expression) if not isinstance(expression, Statement): - raise ValueError(u"Add '{}' must be expression or statement, not {}" - u"".format(expression, type(expression))) + raise ValueError("Add '{}' must be expression or statement, not {}" + "".format(expression, type(expression))) self.global_statements.append(expression) _LOGGER.debug("Adding global: %s", expression) return expression def add_library(self, library): if not isinstance(library, Library): - raise ValueError(u"Library {} must be instance of Library, not {}" - u"".format(library, type(library))) + raise ValueError("Library {} must be instance of Library, not {}" + "".format(library, type(library))) _LOGGER.debug("Adding library: %s", library) for other in self.libraries[:]: if other.name != library.name: @@ -689,9 +682,9 @@ class EsphomeCore(object): if other.version == library.version: break - raise ValueError(u"Version pinning failed! Libraries {} and {} " - u"requested with conflicting versions!" - u"".format(library, other)) + raise ValueError("Version pinning failed! Libraries {} and {} " + "requested with conflicting versions!" + "".format(library, other)) else: self.libraries.append(library) return library @@ -702,20 +695,20 @@ class EsphomeCore(object): return build_flag def add_define(self, define): - if isinstance(define, string_types): + if isinstance(define, str): define = Define(define) elif isinstance(define, Define): pass else: - raise ValueError(u"Define {} must be string or Define, not {}" - u"".format(define, type(define))) + raise ValueError("Define {} must be string or Define, not {}" + "".format(define, type(define))) self.defines.add(define) _LOGGER.debug("Adding define: %s", define) return define def get_variable(self, id): if not isinstance(id, ID): - raise ValueError("ID {!r} must be of type ID!".format(id)) + raise ValueError(f"ID {id!r} must be of type ID!") while True: if id in self.variables: yield self.variables[id] @@ -735,7 +728,7 @@ class EsphomeCore(object): def register_variable(self, id, obj): if id in self.variables: - raise EsphomeError("ID {} is already registered".format(id)) + raise EsphomeError(f"ID {id} is already registered") _LOGGER.debug("Registered variable %s of type %s", id.id, id.type) self.variables[id] = obj @@ -748,10 +741,10 @@ class EsphomeCore(object): main_code = [] for exp in self.main_statements: - text = text_type(statement(exp)) + text = str(statement(exp)) text = text.rstrip() main_code.append(text) - return u'\n'.join(main_code) + u'\n\n' + return '\n'.join(main_code) + '\n\n' @property def cpp_global_section(self): @@ -759,17 +752,17 @@ class EsphomeCore(object): global_code = [] for exp in self.global_statements: - text = text_type(statement(exp)) + text = str(statement(exp)) text = text.rstrip() global_code.append(text) - return u'\n'.join(global_code) + u'\n' + return '\n'.join(global_code) + '\n' class AutoLoad(OrderedDict): pass -class EnumValue(object): +class EnumValue: """Special type used by ESPHome to mark enum values for cv.enum.""" @property def enum_value(self): diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index 26662b0061..6297013247 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -138,6 +138,9 @@ float Component::get_actual_setup_priority() const { return this->setup_priority_override_; } void Component::set_setup_priority(float priority) { this->setup_priority_override_ = priority; } + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wpmf-conversions" bool Component::has_overridden_loop() const { #ifdef CLANG_TIDY bool loop_overridden = true; @@ -148,6 +151,7 @@ bool Component::has_overridden_loop() const { #endif return loop_overridden || call_loop_overridden; } +#pragma GCC diagnostic pop PollingComponent::PollingComponent(uint32_t update_interval) : Component(), update_interval_(update_interval) {} diff --git a/esphome/core_config.py b/esphome/core_config.py index 63092891d3..5cfff6c4d9 100644 --- a/esphome/core_config.py +++ b/esphome/core_config.py @@ -37,8 +37,8 @@ def validate_board(value): raise NotImplementedError if value not in board_pins: - raise cv.Invalid(u"Could not find board '{}'. Valid boards are {}".format( - value, u', '.join(sorted(board_pins.keys())))) + raise cv.Invalid("Could not find board '{}'. Valid boards are {}".format( + value, ', '.join(sorted(board_pins.keys())))) return value @@ -108,8 +108,8 @@ def valid_include(value): value = cv.file_(value) _, ext = os.path.splitext(value) if ext not in VALID_INCLUDE_EXTS: - raise cv.Invalid(u"Include has invalid file extension {} - valid extensions are {}" - u"".format(ext, ', '.join(VALID_INCLUDE_EXTS))) + raise cv.Invalid("Include has invalid file extension {} - valid extensions are {}" + "".format(ext, ', '.join(VALID_INCLUDE_EXTS))) return value @@ -184,7 +184,7 @@ def include_file(path, basename): _, ext = os.path.splitext(path) if ext in ['.h', '.hpp', '.tcc']: # Header, add include statement - cg.add_global(cg.RawStatement(u'#include "{}"'.format(basename))) + cg.add_global(cg.RawStatement(f'#include "{basename}"')) @coroutine_with_priority(-1000.0) @@ -238,7 +238,7 @@ def to_code(config): ld_script = ld_scripts[1] if ld_script is not None: - cg.add_build_flag('-Wl,-T{}'.format(ld_script)) + cg.add_build_flag(f'-Wl,-T{ld_script}') cg.add_build_flag('-fno-exceptions') diff --git a/esphome/cpp_generator.py b/esphome/cpp_generator.py index 09b542b3cc..b5239e9413 100644 --- a/esphome/cpp_generator.py +++ b/esphome/cpp_generator.py @@ -10,22 +10,21 @@ from esphome.core import ( # noqa TimePeriodMilliseconds, TimePeriodMinutes, TimePeriodSeconds, coroutine, Library, Define, EnumValue) from esphome.helpers import cpp_string_escape, indent_all_but_first_and_last -from esphome.py_compat import integer_types, string_types, text_type from esphome.util import OrderedDict -class Expression(object): +class Expression: def __str__(self): raise NotImplementedError -SafeExpType = Union[Expression, bool, str, text_type, int, float, TimePeriod, +SafeExpType = Union[Expression, bool, str, str, int, float, TimePeriod, Type[bool], Type[int], Type[float], List[Any]] class RawExpression(Expression): - def __init__(self, text): # type: (Union[str, unicode]) -> None - super(RawExpression, self).__init__() + def __init__(self, text): # type: (Union[str, str]) -> None + super().__init__() self.text = text def __str__(self): @@ -35,7 +34,7 @@ class RawExpression(Expression): # pylint: disable=redefined-builtin class AssignmentExpression(Expression): def __init__(self, type, modifier, name, rhs, obj): - super(AssignmentExpression, self).__init__() + super().__init__() self.type = type self.modifier = modifier self.name = name @@ -44,24 +43,24 @@ class AssignmentExpression(Expression): def __str__(self): if self.type is None: - return u"{} = {}".format(self.name, self.rhs) - return u"{} {}{} = {}".format(self.type, self.modifier, self.name, self.rhs) + return f"{self.name} = {self.rhs}" + return f"{self.type} {self.modifier}{self.name} = {self.rhs}" class VariableDeclarationExpression(Expression): def __init__(self, type, modifier, name): - super(VariableDeclarationExpression, self).__init__() + super().__init__() self.type = type self.modifier = modifier self.name = name def __str__(self): - return u"{} {}{}".format(self.type, self.modifier, self.name) + return f"{self.type} {self.modifier}{self.name}" class ExpressionList(Expression): def __init__(self, *args): - super(ExpressionList, self).__init__() + super().__init__() # Remove every None on end args = list(args) while args and args[-1] is None: @@ -69,7 +68,7 @@ class ExpressionList(Expression): self.args = [safe_exp(arg) for arg in args] def __str__(self): - text = u", ".join(text_type(x) for x in self.args) + text = ", ".join(str(x) for x in self.args) return indent_all_but_first_and_last(text) def __iter__(self): @@ -78,11 +77,11 @@ class ExpressionList(Expression): class TemplateArguments(Expression): def __init__(self, *args): # type: (*SafeExpType) -> None - super(TemplateArguments, self).__init__() + super().__init__() self.args = ExpressionList(*args) def __str__(self): - return u'<{}>'.format(self.args) + return f'<{self.args}>' def __iter__(self): return iter(self.args) @@ -90,7 +89,7 @@ class TemplateArguments(Expression): class CallExpression(Expression): def __init__(self, base, *args): # type: (Expression, *SafeExpType) -> None - super(CallExpression, self).__init__() + super().__init__() self.base = base if args and isinstance(args[0], TemplateArguments): self.template_args = args[0] @@ -101,13 +100,13 @@ class CallExpression(Expression): def __str__(self): if self.template_args is not None: - return u'{}{}({})'.format(self.base, self.template_args, self.args) - return u'{}({})'.format(self.base, self.args) + return f'{self.base}{self.template_args}({self.args})' + return f'{self.base}({self.args})' class StructInitializer(Expression): def __init__(self, base, *args): # type: (Expression, *Tuple[str, SafeExpType]) -> None - super(StructInitializer, self).__init__() + super().__init__() self.base = base if not isinstance(args, OrderedDict): args = OrderedDict(args) @@ -119,16 +118,16 @@ class StructInitializer(Expression): self.args[key] = exp def __str__(self): - cpp = u'{}{{\n'.format(self.base) + cpp = f'{self.base}{{\n' for key, value in self.args.items(): - cpp += u' .{} = {},\n'.format(key, value) - cpp += u'}' + cpp += f' .{key} = {value},\n' + cpp += '}' return cpp class ArrayInitializer(Expression): def __init__(self, *args, **kwargs): # type: (*Any, **Any) -> None - super(ArrayInitializer, self).__init__() + super().__init__() self.multiline = kwargs.get('multiline', False) self.args = [] for arg in args: @@ -139,30 +138,30 @@ class ArrayInitializer(Expression): def __str__(self): if not self.args: - return u'{}' + return '{}' if self.multiline: - cpp = u'{\n' + cpp = '{\n' for arg in self.args: - cpp += u' {},\n'.format(arg) - cpp += u'}' + cpp += f' {arg},\n' + cpp += '}' else: - cpp = u'{' + u', '.join(str(arg) for arg in self.args) + u'}' + cpp = '{' + ', '.join(str(arg) for arg in self.args) + '}' return cpp class ParameterExpression(Expression): def __init__(self, type, id): - super(ParameterExpression, self).__init__() + super().__init__() self.type = safe_exp(type) self.id = id def __str__(self): - return u"{} {}".format(self.type, self.id) + return f"{self.type} {self.id}" class ParameterListExpression(Expression): def __init__(self, *parameters): - super(ParameterListExpression, self).__init__() + super().__init__() self.parameters = [] for parameter in parameters: if not isinstance(parameter, ParameterExpression): @@ -170,12 +169,12 @@ class ParameterListExpression(Expression): self.parameters.append(parameter) def __str__(self): - return u", ".join(text_type(x) for x in self.parameters) + return ", ".join(str(x) for x in self.parameters) class LambdaExpression(Expression): def __init__(self, parts, parameters, capture='=', return_type=None): - super(LambdaExpression, self).__init__() + super().__init__() self.parts = parts if not isinstance(parameters, ParameterListExpression): parameters = ParameterListExpression(*parameters) @@ -184,15 +183,15 @@ class LambdaExpression(Expression): self.return_type = safe_exp(return_type) if return_type is not None else None def __str__(self): - cpp = u'[{}]({})'.format(self.capture, self.parameters) + cpp = f'[{self.capture}]({self.parameters})' if self.return_type is not None: - cpp += u' -> {}'.format(self.return_type) - cpp += u' {{\n{}\n}}'.format(self.content) + cpp += f' -> {self.return_type}' + cpp += f' {{\n{self.content}\n}}' return indent_all_but_first_and_last(cpp) @property def content(self): - return u''.join(text_type(part) for part in self.parts) + return ''.join(str(part) for part in self.parts) class Literal(Expression): @@ -201,41 +200,41 @@ class Literal(Expression): class StringLiteral(Literal): - def __init__(self, string): # type: (Union[str, unicode]) -> None - super(StringLiteral, self).__init__() + def __init__(self, string): # type: (Union[str, str]) -> None + super().__init__() self.string = string def __str__(self): - return u'{}'.format(cpp_string_escape(self.string)) + return '{}'.format(cpp_string_escape(self.string)) class IntLiteral(Literal): - def __init__(self, i): # type: (Union[int, long]) -> None - super(IntLiteral, self).__init__() + def __init__(self, i): # type: (Union[int]) -> None + super().__init__() self.i = i def __str__(self): if self.i > 4294967295: - return u'{}ULL'.format(self.i) + return f'{self.i}ULL' if self.i > 2147483647: - return u'{}UL'.format(self.i) + return f'{self.i}UL' if self.i < -2147483648: - return u'{}LL'.format(self.i) - return text_type(self.i) + return f'{self.i}LL' + return str(self.i) class BoolLiteral(Literal): def __init__(self, binary): # type: (bool) -> None - super(BoolLiteral, self).__init__() + super().__init__() self.binary = binary def __str__(self): - return u"true" if self.binary else u"false" + return "true" if self.binary else "false" class HexIntLiteral(Literal): def __init__(self, i): # type: (int) -> None - super(HexIntLiteral, self).__init__() + super().__init__() self.i = HexInt(i) def __str__(self): @@ -244,18 +243,18 @@ class HexIntLiteral(Literal): class FloatLiteral(Literal): def __init__(self, value): # type: (float) -> None - super(FloatLiteral, self).__init__() + super().__init__() self.float_ = value def __str__(self): if math.isnan(self.float_): - return u"NAN" - return u"{}f".format(self.float_) + return "NAN" + return f"{self.float_}f" # pylint: disable=bad-continuation def safe_exp( - obj # type: Union[Expression, bool, str, unicode, int, long, float, TimePeriod, list] + obj # type: Union[Expression, bool, str, int, float, TimePeriod, list] ): # type: (...) -> Expression """Try to convert obj to an expression by automatically converting native python types to @@ -269,11 +268,11 @@ def safe_exp( return safe_exp(obj.enum_value) if isinstance(obj, bool): return BoolLiteral(obj) - if isinstance(obj, string_types): + if isinstance(obj, str): return StringLiteral(obj) if isinstance(obj, HexInt): return HexIntLiteral(obj) - if isinstance(obj, integer_types): + if isinstance(obj, int): return IntLiteral(obj) if isinstance(obj, float): return FloatLiteral(obj) @@ -294,15 +293,15 @@ def safe_exp( if obj is float: return float_ if isinstance(obj, ID): - raise ValueError(u"Object {} is an ID. Did you forget to register the variable?" - u"".format(obj)) + raise ValueError("Object {} is an ID. Did you forget to register the variable?" + "".format(obj)) if inspect.isgenerator(obj): - raise ValueError(u"Object {} is a coroutine. Did you forget to await the expression with " - u"'yield'?".format(obj)) - raise ValueError(u"Object is not an expression", obj) + raise ValueError("Object {} is a coroutine. Did you forget to await the expression with " + "'yield'?".format(obj)) + raise ValueError("Object is not an expression", obj) -class Statement(object): +class Statement: def __init__(self): pass @@ -312,7 +311,7 @@ class Statement(object): class RawStatement(Statement): def __init__(self, text): - super(RawStatement, self).__init__() + super().__init__() self.text = text def __str__(self): @@ -321,38 +320,38 @@ class RawStatement(Statement): class ExpressionStatement(Statement): def __init__(self, expression): - super(ExpressionStatement, self).__init__() + super().__init__() self.expression = safe_exp(expression) def __str__(self): - return u"{};".format(self.expression) + return f"{self.expression};" class LineComment(Statement): - def __init__(self, value): # type: (unicode) -> None - super(LineComment, self).__init__() + def __init__(self, value): # type: (str) -> None + super().__init__() self._value = value def __str__(self): - parts = self._value.split(u'\n') - parts = [u'// {}'.format(x) for x in parts] - return u'\n'.join(parts) + parts = self._value.split('\n') + parts = [f'// {x}' for x in parts] + return '\n'.join(parts) class ProgmemAssignmentExpression(AssignmentExpression): def __init__(self, type, name, rhs, obj): - super(ProgmemAssignmentExpression, self).__init__( + super().__init__( type, '', name, rhs, obj ) def __str__(self): type_ = self.type - return u"static const {} {}[] PROGMEM = {}".format(type_, self.name, self.rhs) + return f"static const {type_} {self.name}[] PROGMEM = {self.rhs}" def progmem_array(id, rhs): rhs = safe_exp(rhs) - obj = MockObj(id, u'.') + obj = MockObj(id, '.') assignment = ProgmemAssignmentExpression(id.type, id, rhs, obj) CORE.add(assignment) CORE.register_variable(id, obj) @@ -381,7 +380,7 @@ def variable(id, # type: ID """ assert isinstance(id, ID) rhs = safe_exp(rhs) - obj = MockObj(id, u'.') + obj = MockObj(id, '.') if type is not None: id.type = type assignment = AssignmentExpression(id.type, '', id, rhs, obj) @@ -405,7 +404,7 @@ def Pvariable(id, # type: ID :returns The new variable as a MockObj. """ rhs = safe_exp(rhs) - obj = MockObj(id, u'->') + obj = MockObj(id, '->') if type is not None: id.type = type decl = VariableDeclarationExpression(id.type, '*', id) @@ -594,51 +593,51 @@ class MockObj(Expression): Mostly consists of magic methods that allow ESPHome's codegen syntax. """ - def __init__(self, base, op=u'.'): + def __init__(self, base, op='.'): self.base = base self.op = op - super(MockObj, self).__init__() + super().__init__() def __getattr__(self, attr): # type: (str) -> MockObj - next_op = u'.' - if attr.startswith(u'P') and self.op not in ['::', '']: + next_op = '.' + if attr.startswith('P') and self.op not in ['::', '']: attr = attr[1:] - next_op = u'->' - if attr.startswith(u'_'): + next_op = '->' + if attr.startswith('_'): attr = attr[1:] - return MockObj(u'{}{}{}'.format(self.base, self.op, attr), next_op) + return MockObj(f'{self.base}{self.op}{attr}', next_op) def __call__(self, *args): # type: (SafeExpType) -> MockObj call = CallExpression(self.base, *args) return MockObj(call, self.op) - def __str__(self): # type: () -> unicode - return text_type(self.base) + def __str__(self): # type: () -> str + return str(self.base) def __repr__(self): - return u'MockObj<{}>'.format(text_type(self.base)) + return 'MockObj<{}>'.format(str(self.base)) @property def _(self): # type: () -> MockObj - return MockObj(u'{}{}'.format(self.base, self.op)) + return MockObj(f'{self.base}{self.op}') @property def new(self): # type: () -> MockObj - return MockObj(u'new {}'.format(self.base), u'->') + return MockObj(f'new {self.base}', '->') def template(self, *args): # type: (*SafeExpType) -> MockObj if len(args) != 1 or not isinstance(args[0], TemplateArguments): args = TemplateArguments(*args) else: args = args[0] - return MockObj(u'{}{}'.format(self.base, args)) + return MockObj(f'{self.base}{args}') def namespace(self, name): # type: (str) -> MockObj - return MockObj(u'{}{}'.format(self._, name), u'::') + return MockObj(f'{self._}{name}', '::') def class_(self, name, *parents): # type: (str, *MockObjClass) -> MockObjClass op = '' if self.op == '' else '::' - return MockObjClass(u'{}{}{}'.format(self.base, op, name), u'.', parents=parents) + return MockObjClass(f'{self.base}{op}{name}', '.', parents=parents) def struct(self, name): # type: (str) -> MockObjClass return self.class_(name) @@ -648,24 +647,24 @@ class MockObj(Expression): def operator(self, name): # type: (str) -> MockObj if name == 'ref': - return MockObj(u'{} &'.format(self.base), u'') + return MockObj(f'{self.base} &', '') if name == 'ptr': - return MockObj(u'{} *'.format(self.base), u'') + return MockObj(f'{self.base} *', '') if name == "const": - return MockObj(u'const {}'.format(self.base), u'') + return MockObj(f'const {self.base}', '') raise NotImplementedError @property def using(self): # type: () -> MockObj assert self.op == '::' - return MockObj(u'using namespace {}'.format(self.base)) + return MockObj(f'using namespace {self.base}') def __getitem__(self, item): # type: (Union[str, Expression]) -> MockObj - next_op = u'.' - if isinstance(item, str) and item.startswith(u'P'): + next_op = '.' + if isinstance(item, str) and item.startswith('P'): item = item[1:] - next_op = u'->' - return MockObj(u'{}[{}]'.format(self.base, item), next_op) + next_op = '->' + return MockObj(f'{self.base}[{item}]', next_op) class MockObjEnum(MockObj): @@ -679,13 +678,13 @@ class MockObjEnum(MockObj): kwargs['base'] = base MockObj.__init__(self, *args, **kwargs) - def __str__(self): # type: () -> unicode + def __str__(self): # type: () -> str if self._is_class: - return super(MockObjEnum, self).__str__() - return u'{}{}{}'.format(self.base, self.op, self._enum) + return super().__str__() + return f'{self.base}{self.op}{self._enum}' def __repr__(self): - return u'MockObj<{}>'.format(text_type(self.base)) + return 'MockObj<{}>'.format(str(self.base)) class MockObjClass(MockObj): @@ -716,7 +715,7 @@ class MockObjClass(MockObj): args = args[0] new_parents = self._parents[:] new_parents.append(self) - return MockObjClass(u'{}{}'.format(self.base, args), parents=new_parents) + return MockObjClass(f'{self.base}{args}', parents=new_parents) def __repr__(self): - return u'MockObjClass<{}, parents={}>'.format(text_type(self.base), self._parents) + return 'MockObjClass<{}, parents={}>'.format(str(self.base), self._parents) diff --git a/esphome/cpp_helpers.py b/esphome/cpp_helpers.py index 39ac8e7118..f01981acc8 100644 --- a/esphome/cpp_helpers.py +++ b/esphome/cpp_helpers.py @@ -1,9 +1,10 @@ from esphome.const import CONF_INVERTED, CONF_MODE, CONF_NUMBER, CONF_SETUP_PRIORITY, \ CONF_UPDATE_INTERVAL, CONF_TYPE_ID -from esphome.core import coroutine, ID, CORE +# pylint: disable=unused-import +from esphome.core import coroutine, ID, CORE, ConfigType from esphome.cpp_generator import RawExpression, add, get_variable from esphome.cpp_types import App, GPIOPin -from esphome.py_compat import text_type +from esphome.util import Registry, RegistryEntry @coroutine @@ -35,11 +36,11 @@ def register_component(var, config): :param var: The variable representing the component. :param config: The configuration for the component. """ - id_ = text_type(var.base) + id_ = str(var.base) if id_ not in CORE.component_ids: - raise ValueError(u"Component ID {} was not declared to inherit from Component, " - u"or was registered twice. Please create a bug report with your " - u"configuration.".format(id_)) + raise ValueError("Component ID {} was not declared to inherit from Component, " + "or was registered twice. Please create a bug report with your " + "configuration.".format(id_)) CORE.component_ids.remove(id_) if CONF_SETUP_PRIORITY in config: add(var.set_setup_priority(config[CONF_SETUP_PRIORITY])) @@ -59,7 +60,7 @@ def register_parented(var, value): def extract_registry_entry_config(registry, full_config): - # type: ('Registry', 'ConfigType') -> 'RegistryEntry' + # type: (Registry, ConfigType) -> RegistryEntry key, config = next((k, v) for k, v in full_config.items() if k in registry) return registry[key], config diff --git a/esphome/dashboard/dashboard.py b/esphome/dashboard/dashboard.py index c934626da8..8aea841247 100644 --- a/esphome/dashboard/dashboard.py +++ b/esphome/dashboard/dashboard.py @@ -1,5 +1,4 @@ # pylint: disable=wrong-import-position -from __future__ import print_function import codecs import collections @@ -29,7 +28,6 @@ import tornado.websocket from esphome import const, util from esphome.__main__ import get_serial_ports from esphome.helpers import mkdir_p, get_bool_env, run_system_command -from esphome.py_compat import IS_PY2, decode_text, encode_text from esphome.storage_json import EsphomeStorageJSON, StorageJSON, \ esphome_storage_path, ext_storage_path, trash_storage_path from esphome.util import shlex_quote @@ -42,7 +40,7 @@ from esphome.zeroconf import DashboardStatus, Zeroconf _LOGGER = logging.getLogger(__name__) -class DashboardSettings(object): +class DashboardSettings: def __init__(self): self.config_dir = '' self.password_digest = '' @@ -58,10 +56,7 @@ class DashboardSettings(object): self.username = args.username or os.getenv('USERNAME', '') self.using_password = bool(password) if self.using_password: - if IS_PY2: - self.password_digest = hmac.new(password).digest() - else: - self.password_digest = hmac.new(password.encode()).digest() + self.password_digest = hmac.new(password.encode()).digest() self.config_dir = args.configuration[0] @property @@ -88,8 +83,8 @@ class DashboardSettings(object): if username != self.username: return False - password_digest = hmac.new(encode_text(password)).digest() - return hmac.compare_digest(self.password_digest, password_digest) + password = hmac.new(password.encode()).digest() + return username == self.username and hmac.compare_digest(self.password_digest, password) def rel_path(self, *args): return os.path.join(self.config_dir, *args) @@ -100,10 +95,7 @@ class DashboardSettings(object): settings = DashboardSettings() -if IS_PY2: - cookie_authenticated_yes = 'yes' -else: - cookie_authenticated_yes = b'yes' +cookie_authenticated_yes = b'yes' def template_args(): @@ -181,7 +173,7 @@ def websocket_method(name): @websocket_class class EsphomeCommandWebSocket(tornado.websocket.WebSocketHandler): def __init__(self, application, request, **kwargs): - super(EsphomeCommandWebSocket, self).__init__(application, request, **kwargs) + super().__init__(application, request, **kwargs) self._proc = None self._is_closed = False @@ -204,7 +196,7 @@ class EsphomeCommandWebSocket(tornado.websocket.WebSocketHandler): # spawn can only be called once return command = self.build_command(json_message) - _LOGGER.info(u"Running command '%s'", ' '.join(shlex_quote(x) for x in command)) + _LOGGER.info("Running command '%s'", ' '.join(shlex_quote(x) for x in command)) self._proc = tornado.process.Subprocess(command, stdout=tornado.process.Subprocess.STREAM, stderr=subprocess.STDOUT, @@ -227,10 +219,7 @@ class EsphomeCommandWebSocket(tornado.websocket.WebSocketHandler): @tornado.gen.coroutine def _redirect_stdout(self): - if IS_PY2: - reg = '[\n\r]' - else: - reg = b'[\n\r]' + reg = b'[\n\r]' while True: try: @@ -336,8 +325,8 @@ class WizardRequestHandler(BaseHandler): def post(self): from esphome import wizard - kwargs = {k: u''.join(decode_text(x) for x in v) for k, v in self.request.arguments.items()} - destination = settings.rel_path(kwargs['name'] + u'.yaml') + kwargs = {k: ''.join(str(x) for x in v) for k, v in self.request.arguments.items()} + destination = settings.rel_path(kwargs['name'] + '.yaml') wizard.wizard_write(path=destination, **kwargs) self.redirect('./?begin=True') @@ -355,8 +344,8 @@ class DownloadBinaryRequestHandler(BaseHandler): path = storage_json.firmware_bin_path self.set_header('Content-Type', 'application/octet-stream') - filename = '{}.bin'.format(storage_json.name) - self.set_header("Content-Disposition", 'attachment; filename="{}"'.format(filename)) + filename = f'{storage_json.name}.bin' + self.set_header("Content-Disposition", f'attachment; filename="{filename}"') with open(path, 'rb') as f: while True: data = f.read(16384) @@ -371,7 +360,7 @@ def _list_dashboard_entries(): return [DashboardEntry(file) for file in files] -class DashboardEntry(object): +class DashboardEntry: def __init__(self, path): self.path = path self._storage = None @@ -609,8 +598,8 @@ class LoginHandler(BaseHandler): 'X-HASSIO-KEY': os.getenv('HASSIO_TOKEN'), } data = { - 'username': decode_text(self.get_argument('username', '')), - 'password': decode_text(self.get_argument('password', '')) + 'username': self.get_argument('username', ''), + 'password': self.get_argument('password', '') } try: req = requests.post('http://hassio/auth', headers=headers, data=data) @@ -627,8 +616,8 @@ class LoginHandler(BaseHandler): self.render_login_page(error="Invalid username or password") def post_native_login(self): - username = decode_text(self.get_argument("username", '')) - password = decode_text(self.get_argument("password", '')) + username = self.get_argument("username", '') + password = self.get_argument("password", '') if settings.check_password(username, password): self.set_secure_cookie("authenticated", cookie_authenticated_yes) self.redirect("/") @@ -663,7 +652,7 @@ def get_static_file_url(name): with open(path, 'rb') as f_handle: hash_ = hashlib.md5(f_handle.read()).hexdigest()[:8] _STATIC_FILE_HASHES[name] = hash_ - return u'./static/{}?hash={}'.format(name, hash_) + return f'./static/{name}?hash={hash_}' def make_app(debug=False): @@ -754,7 +743,7 @@ def start_web_server(args): if args.open_ui: import webbrowser - webbrowser.open('localhost:{}'.format(args.port)) + webbrowser.open(f'localhost:{args.port}') if settings.status_use_ping: status_thread = PingStatusThread() diff --git a/esphome/espota2.py b/esphome/espota2.py index 40417b9ab2..edfa4e63e6 100644 --- a/esphome/espota2.py +++ b/esphome/espota2.py @@ -7,7 +7,6 @@ import time from esphome.core import EsphomeError from esphome.helpers import is_ip_address, resolve_ip_address -from esphome.py_compat import IS_PY2, char_to_byte RESPONSE_OK = 0 RESPONSE_REQUEST_AUTH = 1 @@ -38,7 +37,7 @@ MAGIC_BYTES = [0x6C, 0x26, 0xF7, 0x5C, 0x45] _LOGGER = logging.getLogger(__name__) -class ProgressBar(object): +class ProgressBar: def __init__(self): self.last_progress = None @@ -72,33 +71,31 @@ def recv_decode(sock, amount, decode=True): data = sock.recv(amount) if not decode: return data - return [char_to_byte(x) for x in data] + return list(data) def receive_exactly(sock, amount, msg, expect, decode=True): if decode: data = [] - elif IS_PY2: - data = '' else: data = b'' try: data += recv_decode(sock, 1, decode=decode) - except socket.error as err: - raise OTAError("Error receiving acknowledge {}: {}".format(msg, err)) + except OSError as err: + raise OTAError(f"Error receiving acknowledge {msg}: {err}") try: check_error(data, expect) except OTAError as err: sock.close() - raise OTAError("Error {}: {}".format(msg, err)) + raise OTAError(f"Error {msg}: {err}") while len(data) < amount: try: data += recv_decode(sock, amount - len(data), decode=decode) - except socket.error as err: - raise OTAError("Error receiving {}: {}".format(msg, err)) + except OSError as err: + raise OTAError(f"Error receiving {msg}: {err}") return data @@ -145,22 +142,16 @@ def check_error(data, expect): def send_check(sock, data, msg): try: - if IS_PY2: - if isinstance(data, (list, tuple)): - data = ''.join([chr(x) for x in data]) - elif isinstance(data, int): - data = chr(data) - else: - if isinstance(data, (list, tuple)): - data = bytes(data) - elif isinstance(data, int): - data = bytes([data]) - elif isinstance(data, str): - data = data.encode('utf8') + if isinstance(data, (list, tuple)): + data = bytes(data) + elif isinstance(data, int): + data = bytes([data]) + elif isinstance(data, str): + data = data.encode('utf8') sock.sendall(data) - except socket.error as err: - raise OTAError("Error sending {}: {}".format(msg, err)) + except OSError as err: + raise OTAError(f"Error sending {msg}: {err}") def perform_ota(sock, password, file_handle, filename): @@ -176,7 +167,7 @@ def perform_ota(sock, password, file_handle, filename): _, version = receive_exactly(sock, 2, 'version', RESPONSE_OK) if version != OTA_VERSION_1_0: - raise OTAError("Unsupported OTA version {}".format(version)) + raise OTAError(f"Unsupported OTA version {version}") # Features send_check(sock, 0x00, 'features') @@ -186,9 +177,7 @@ def perform_ota(sock, password, file_handle, filename): if auth == RESPONSE_REQUEST_AUTH: if not password: raise OTAError("ESP requests password, but no password given!") - nonce = receive_exactly(sock, 32, 'authentication nonce', [], decode=False) - if not IS_PY2: - nonce = nonce.decode() + nonce = receive_exactly(sock, 32, 'authentication nonce', [], decode=False).decode() _LOGGER.debug("Auth: Nonce is %s", nonce) cnonce = hashlib.md5(str(random.random()).encode()).hexdigest() _LOGGER.debug("Auth: CNonce is %s", cnonce) @@ -235,9 +224,9 @@ def perform_ota(sock, password, file_handle, filename): try: sock.sendall(chunk) - except socket.error as err: + except OSError as err: sys.stderr.write('\n') - raise OTAError("Error sending data: {}".format(err)) + raise OTAError(f"Error sending data: {err}") progress.update(offset / float(file_size)) progress.done() @@ -277,7 +266,7 @@ def run_ota_impl_(remote_host, remote_port, password, filename): sock.settimeout(10.0) try: sock.connect((ip, remote_port)) - except socket.error as err: + except OSError as err: sock.close() _LOGGER.error("Connecting to %s:%s failed: %s", remote_host, remote_port, err) return 1 diff --git a/esphome/helpers.py b/esphome/helpers.py index 179452c353..6413a25e01 100644 --- a/esphome/helpers.py +++ b/esphome/helpers.py @@ -1,12 +1,8 @@ -from __future__ import print_function - import codecs import logging import os -from esphome.py_compat import char_to_byte, text_type, IS_PY2, encode_text - _LOGGER = logging.getLogger(__name__) @@ -18,24 +14,24 @@ def ensure_unique_string(preferred_string, current_strings): while test_string in current_strings_set: tries += 1 - test_string = u"{}_{}".format(preferred_string, tries) + test_string = f"{preferred_string}_{tries}" return test_string -def indent_all_but_first_and_last(text, padding=u' '): +def indent_all_but_first_and_last(text, padding=' '): lines = text.splitlines(True) if len(lines) <= 2: return text - return lines[0] + u''.join(padding + line for line in lines[1:-1]) + lines[-1] + return lines[0] + ''.join(padding + line for line in lines[1:-1]) + lines[-1] -def indent_list(text, padding=u' '): +def indent_list(text, padding=' '): return [padding + line for line in text.splitlines()] -def indent(text, padding=u' '): - return u'\n'.join(indent_list(text, padding)) +def indent(text, padding=' '): + return '\n'.join(indent_list(text, padding)) # From https://stackoverflow.com/a/14945195/8924614 @@ -43,17 +39,16 @@ def cpp_string_escape(string, encoding='utf-8'): def _should_escape(byte): # type: (int) -> bool if not 32 <= byte < 127: return True - if byte in (char_to_byte('\\'), char_to_byte('"')): + if byte in (ord('\\'), ord('"')): return True return False - if isinstance(string, text_type): + if isinstance(string, str): string = string.encode(encoding) result = '' for character in string: - character = char_to_byte(character) if _should_escape(character): - result += '\\%03o' % character + result += f'\\{character:03o}' else: result += chr(character) return '"' + result + '"' @@ -91,7 +86,7 @@ def mkdir_p(path): pass else: from esphome.core import EsphomeError - raise EsphomeError(u"Error creating directories {}: {}".format(path, err)) + raise EsphomeError(f"Error creating directories {path}: {err}") def is_ip_address(host): @@ -118,7 +113,7 @@ def _resolve_with_zeroconf(host): try: info = zc.resolve_host(host + '.') except Exception as err: - raise EsphomeError("Error resolving mDNS hostname: {}".format(err)) + raise EsphomeError(f"Error resolving mDNS hostname: {err}") finally: zc.close() if info is None: @@ -141,7 +136,7 @@ def resolve_ip_address(host): try: return socket.gethostbyname(host) - except socket.error as err: + except OSError as err: errs.append(str(err)) raise EsphomeError("Error resolving IP address: {}" "".format(', '.join(errs))) @@ -167,10 +162,10 @@ def read_file(path): return f_handle.read() except OSError as err: from esphome.core import EsphomeError - raise EsphomeError(u"Error reading file {}: {}".format(path, err)) + raise EsphomeError(f"Error reading file {path}: {err}") except UnicodeDecodeError as err: from esphome.core import EsphomeError - raise EsphomeError(u"Error reading file {}: {}".format(path, err)) + raise EsphomeError(f"Error reading file {path}: {err}") def _write_file(path, text): @@ -179,20 +174,17 @@ def _write_file(path, text): mkdir_p(directory) tmp_path = None - data = encode_text(text) + data = text + if isinstance(text, str): + data = text.encode() try: with tempfile.NamedTemporaryFile(mode="wb", dir=directory, delete=False) as f_handle: tmp_path = f_handle.name f_handle.write(data) # Newer tempfile implementations create the file with mode 0o600 os.chmod(tmp_path, 0o644) - if IS_PY2: - if os.path.exists(path): - os.remove(path) - os.rename(tmp_path, path) - else: - # If destination exists, will be overwritten - os.replace(tmp_path, path) + # If destination exists, will be overwritten + os.replace(tmp_path, path) finally: if tmp_path is not None and os.path.exists(tmp_path): try: @@ -206,7 +198,7 @@ def write_file(path, text): _write_file(path, text) except OSError: from esphome.core import EsphomeError - raise EsphomeError(u"Could not write file at {}".format(path)) + raise EsphomeError(f"Could not write file at {path}") def write_file_if_changed(path, text): @@ -226,7 +218,7 @@ def copy_file_if_changed(src, dst): shutil.copy(src, dst) except OSError as err: from esphome.core import EsphomeError - raise EsphomeError(u"Error copying file {} to {}: {}".format(src, dst, err)) + raise EsphomeError(f"Error copying file {src} to {dst}: {err}") def list_starts_with(list_, sub): @@ -273,10 +265,6 @@ _TYPE_OVERLOADS = { list: type('EList', (list,), dict()), } -if IS_PY2: - _TYPE_OVERLOADS[long] = type('long', (long,), dict()) - _TYPE_OVERLOADS[unicode] = type('unicode', (unicode,), dict()) - # cache created classes here _CLASS_LOOKUP = {} diff --git a/esphome/legacy.py b/esphome/legacy.py index 0e19545d68..27373ee1a3 100644 --- a/esphome/legacy.py +++ b/esphome/legacy.py @@ -1,4 +1,3 @@ -from __future__ import print_function import sys diff --git a/esphome/mqtt.py b/esphome/mqtt.py index 77eb941363..cbcf067c44 100644 --- a/esphome/mqtt.py +++ b/esphome/mqtt.py @@ -1,9 +1,6 @@ -from __future__ import print_function - from datetime import datetime import hashlib import logging -import socket import ssl import sys import time @@ -15,7 +12,6 @@ from esphome.const import CONF_BROKER, CONF_DISCOVERY_PREFIX, CONF_ESPHOME, \ CONF_TOPIC, CONF_TOPIC_PREFIX, CONF_USERNAME from esphome.core import CORE, EsphomeError from esphome.helpers import color -from esphome.py_compat import decode_text from esphome.util import safe_print _LOGGER = logging.getLogger(__name__) @@ -37,7 +33,7 @@ def initialize(config, subscriptions, on_message, username, password, client_id) if client.reconnect() == 0: _LOGGER.info("Successfully reconnected to the MQTT server") break - except socket.error: + except OSError: pass wait_time = min(2**tries, 300) @@ -47,7 +43,7 @@ def initialize(config, subscriptions, on_message, username, password, client_id) time.sleep(wait_time) tries += 1 - client = mqtt.Client(client_id or u'') + client = mqtt.Client(client_id or '') client.on_connect = on_connect client.on_message = on_message client.on_disconnect = on_disconnect @@ -70,8 +66,8 @@ def initialize(config, subscriptions, on_message, username, password, client_id) host = str(config[CONF_MQTT][CONF_BROKER]) port = int(config[CONF_MQTT][CONF_PORT]) client.connect(host, port) - except socket.error as err: - raise EsphomeError("Cannot connect to MQTT broker: {}".format(err)) + except OSError as err: + raise EsphomeError(f"Cannot connect to MQTT broker: {err}") try: client.loop_forever() @@ -88,17 +84,17 @@ def show_logs(config, topic=None, username=None, password=None, client_id=None): if CONF_LOG_TOPIC in conf: topic = config[CONF_MQTT][CONF_LOG_TOPIC][CONF_TOPIC] elif CONF_TOPIC_PREFIX in config[CONF_MQTT]: - topic = config[CONF_MQTT][CONF_TOPIC_PREFIX] + u'/debug' + topic = config[CONF_MQTT][CONF_TOPIC_PREFIX] + '/debug' else: - topic = config[CONF_ESPHOME][CONF_NAME] + u'/debug' + topic = config[CONF_ESPHOME][CONF_NAME] + '/debug' else: - _LOGGER.error(u"MQTT isn't setup, can't start MQTT logs") + _LOGGER.error("MQTT isn't setup, can't start MQTT logs") return 1 - _LOGGER.info(u"Starting log output from %s", topic) + _LOGGER.info("Starting log output from %s", topic) def on_message(client, userdata, msg): - time_ = datetime.now().time().strftime(u'[%H:%M:%S]') - payload = decode_text(msg.payload) + time_ = datetime.now().time().strftime('[%H:%M:%S]') + payload = msg.payload.decode(errors='backslashreplace') message = time_ + payload safe_print(message) @@ -107,20 +103,20 @@ def show_logs(config, topic=None, username=None, password=None, client_id=None): def clear_topic(config, topic, username=None, password=None, client_id=None): if topic is None: - discovery_prefix = config[CONF_MQTT].get(CONF_DISCOVERY_PREFIX, u'homeassistant') + discovery_prefix = config[CONF_MQTT].get(CONF_DISCOVERY_PREFIX, 'homeassistant') name = config[CONF_ESPHOME][CONF_NAME] - topic = u'{}/+/{}/#'.format(discovery_prefix, name) - _LOGGER.info(u"Clearing messages from '%s'", topic) - _LOGGER.info(u"Please close this window when no more messages appear and the " - u"MQTT topic has been cleared of retained messages.") + topic = f'{discovery_prefix}/+/{name}/#' + _LOGGER.info("Clearing messages from '%s'", topic) + _LOGGER.info("Please close this window when no more messages appear and the " + "MQTT topic has been cleared of retained messages.") def on_message(client, userdata, msg): if not msg.payload or not msg.retain: return try: - print(u"Clearing topic {}".format(msg.topic)) + print(f"Clearing topic {msg.topic}") except UnicodeDecodeError: - print(u"Skipping non-UTF-8 topic (prohibited by MQTT standard)") + print("Skipping non-UTF-8 topic (prohibited by MQTT standard)") return client.publish(msg.topic, None, retain=True) @@ -133,14 +129,14 @@ def get_fingerprint(config): _LOGGER.info("Getting fingerprint from %s:%s", addr[0], addr[1]) try: cert_pem = ssl.get_server_certificate(addr) - except IOError as err: + except OSError as err: _LOGGER.error("Unable to connect to server: %s", err) return 1 cert_der = ssl.PEM_cert_to_DER_cert(cert_pem) sha1 = hashlib.sha1(cert_der).hexdigest() - safe_print(u"SHA1 Fingerprint: " + color('cyan', sha1)) - safe_print(u"Copy the string above into mqtt.ssl_fingerprints section of {}" - u"".format(CORE.config_path)) + safe_print("SHA1 Fingerprint: " + color('cyan', sha1)) + safe_print("Copy the string above into mqtt.ssl_fingerprints section of {}" + "".format(CORE.config_path)) return 0 diff --git a/esphome/pins.py b/esphome/pins.py index 9a2cf90984..22192b599e 100644 --- a/esphome/pins.py +++ b/esphome/pins.py @@ -1,5 +1,3 @@ -from __future__ import division - import logging import esphome.config_validation as cv @@ -271,13 +269,13 @@ def _lookup_pin(value): return board_pins[value] if value in base_pins: return base_pins[value] - raise cv.Invalid(u"Cannot resolve pin name '{}' for board {}.".format(value, CORE.board)) + raise cv.Invalid(f"Cannot resolve pin name '{value}' for board {CORE.board}.") def _translate_pin(value): if isinstance(value, dict) or value is None: - raise cv.Invalid(u"This variable only supports pin numbers, not full pin schemas " - u"(with inverted and mode).") + raise cv.Invalid("This variable only supports pin numbers, not full pin schemas " + "(with inverted and mode).") if isinstance(value, int): return value try: @@ -301,27 +299,27 @@ def validate_gpio_pin(value): value = _translate_pin(value) if CORE.is_esp32: if value < 0 or value > 39: - raise cv.Invalid(u"ESP32: Invalid pin number: {}".format(value)) + raise cv.Invalid(f"ESP32: Invalid pin number: {value}") if value in _ESP_SDIO_PINS: raise cv.Invalid("This pin cannot be used on ESP32s and is already used by " "the flash interface (function: {})".format(_ESP_SDIO_PINS[value])) if 9 <= value <= 10: - _LOGGER.warning(u"ESP32: Pin %s (9-10) might already be used by the " - u"flash interface in QUAD IO flash mode.", value) + _LOGGER.warning("ESP32: Pin %s (9-10) might already be used by the " + "flash interface in QUAD IO flash mode.", value) if value in (20, 24, 28, 29, 30, 31): # These pins are not exposed in GPIO mux (reason unknown) # but they're missing from IO_MUX list in datasheet - raise cv.Invalid("The pin GPIO{} is not usable on ESP32s.".format(value)) + raise cv.Invalid(f"The pin GPIO{value} is not usable on ESP32s.") return value if CORE.is_esp8266: if value < 0 or value > 17: - raise cv.Invalid(u"ESP8266: Invalid pin number: {}".format(value)) + raise cv.Invalid(f"ESP8266: Invalid pin number: {value}") if value in _ESP_SDIO_PINS: raise cv.Invalid("This pin cannot be used on ESP8266s and is already used by " "the flash interface (function: {})".format(_ESP_SDIO_PINS[value])) if 9 <= value <= 10: - _LOGGER.warning(u"ESP8266: Pin %s (9-10) might already be used by the " - u"flash interface in QUAD IO flash mode.", value) + _LOGGER.warning("ESP8266: Pin %s (9-10) might already be used by the " + "flash interface in QUAD IO flash mode.", value) return value raise NotImplementedError @@ -349,8 +347,8 @@ def output_pin(value): value = validate_gpio_pin(value) if CORE.is_esp32: if 34 <= value <= 39: - raise cv.Invalid(u"ESP32: GPIO{} (34-39) can only be used as an " - u"input pin.".format(value)) + raise cv.Invalid("ESP32: GPIO{} (34-39) can only be used as an " + "input pin.".format(value)) return value if CORE.is_esp8266: if value == 17: @@ -364,11 +362,11 @@ def analog_pin(value): if CORE.is_esp32: if 32 <= value <= 39: # ADC1 return value - raise cv.Invalid(u"ESP32: Only pins 32 though 39 support ADC.") + raise cv.Invalid("ESP32: Only pins 32 though 39 support ADC.") if CORE.is_esp8266: if value == 17: # A0 return value - raise cv.Invalid(u"ESP8266: Only pin A0 (GPIO17) supports ADC.") + raise cv.Invalid("ESP8266: Only pin A0 (GPIO17) supports ADC.") raise NotImplementedError diff --git a/esphome/platformio_api.py b/esphome/platformio_api.py index 317670710b..59f5bf20ae 100644 --- a/esphome/platformio_api.py +++ b/esphome/platformio_api.py @@ -1,5 +1,3 @@ -from __future__ import print_function - import json import logging import os @@ -7,7 +5,6 @@ import re import subprocess from esphome.core import CORE -from esphome.py_compat import decode_text from esphome.util import run_external_command, run_external_process _LOGGER = logging.getLogger(__name__) @@ -61,6 +58,7 @@ FILTER_PLATFORMIO_LINES = [ r'Installing dependencies', r'.* @ .* is already installed', r'Building in .* mode', + r'Advanced Memory Usage is available via .*', ] @@ -100,8 +98,7 @@ def run_upload(config, verbose, port): def run_idedata(config): args = ['-t', 'idedata'] - stdout = run_platformio_cli_run(config, False, *args, capture_stdout=True) - stdout = decode_text(stdout) + stdout = run_platformio_cli_run(config, False, *args, capture_stdout=True).decode() match = re.search(r'{\s*".*}', stdout) if match is None: _LOGGER.debug("Could not match IDEData for %s", stdout) @@ -172,7 +169,7 @@ def _decode_pc(config, addr): return command = [idedata.addr2line_path, '-pfiaC', '-e', idedata.firmware_elf_path, addr] try: - translation = decode_text(subprocess.check_output(command)).strip() + translation = subprocess.check_output(command).decode().strip() except Exception: # pylint: disable=broad-except _LOGGER.debug("Caught exception for command %s", command, exc_info=1) return @@ -246,7 +243,7 @@ def process_stacktrace(config, line, backtrace_state): return backtrace_state -class IDEData(object): +class IDEData: def __init__(self, raw): if not isinstance(raw, dict): self.raw = {} diff --git a/esphome/py_compat.py b/esphome/py_compat.py index 6cdaa5b047..e69de29bb2 100644 --- a/esphome/py_compat.py +++ b/esphome/py_compat.py @@ -1,89 +0,0 @@ -import functools -import sys -import codecs - -PYTHON_MAJOR = sys.version_info[0] -IS_PY2 = PYTHON_MAJOR == 2 -IS_PY3 = PYTHON_MAJOR == 3 - - -# pylint: disable=no-else-return -def safe_input(prompt=None): - if IS_PY2: - if prompt is None: - return raw_input() - return raw_input(prompt) - else: - if prompt is None: - return input() - return input(prompt) - - -if IS_PY2: - text_type = unicode - string_types = (str, unicode) - integer_types = (int, long) - binary_type = str -else: - text_type = str - string_types = (str,) - integer_types = (int,) - binary_type = bytes - - -def byte_to_bytes(val): # type: (int) -> bytes - if IS_PY2: - return chr(val) - else: - return bytes([val]) - - -def char_to_byte(val): # type: (str) -> int - if IS_PY2: - if isinstance(val, string_types): - return ord(val) - elif isinstance(val, int): - return val - else: - raise ValueError - else: - if isinstance(val, str): - return ord(val) - elif isinstance(val, int): - return val - else: - raise ValueError - - -def format_bytes(val): - if IS_PY2: - return ' '.join('{:02X}'.format(ord(x)) for x in val) - else: - return ' '.join('{:02X}'.format(x) for x in val) - - -def sort_by_cmp(list_, cmp): - if IS_PY2: - list_.sort(cmp=cmp) - else: - list_.sort(key=functools.cmp_to_key(cmp)) - - -def indexbytes(buf, i): - if IS_PY3: - return buf[i] - else: - return ord(buf[i]) - - -def decode_text(data, encoding='utf-8', errors='strict'): - if isinstance(data, text_type): - return data - return codecs.decode(data, encoding, errors) - - -def encode_text(data, encoding='utf-8', errors='strict'): - if isinstance(data, binary_type): - return data - - return codecs.encode(data, encoding, errors) diff --git a/esphome/storage_json.py b/esphome/storage_json.py index f3e53ce168..b4dc29c9cd 100644 --- a/esphome/storage_json.py +++ b/esphome/storage_json.py @@ -13,17 +13,15 @@ from esphome.helpers import write_file_if_changed from esphome.core import CoreType from typing import Any, Optional, List -from esphome.py_compat import text_type - _LOGGER = logging.getLogger(__name__) def storage_path(): # type: () -> str - return CORE.relative_config_path('.esphome', '{}.json'.format(CORE.config_filename)) + return CORE.relative_config_path('.esphome', f'{CORE.config_filename}.json') def ext_storage_path(base_path, config_filename): # type: (str, str) -> str - return os.path.join(base_path, '.esphome', '{}.json'.format(config_filename)) + return os.path.join(base_path, '.esphome', f'{config_filename}.json') def esphome_storage_path(base_path): # type: (str) -> str @@ -35,7 +33,7 @@ def trash_storage_path(base_path): # type: (str) -> str # pylint: disable=too-many-instance-attributes -class StorageJSON(object): +class StorageJSON: def __init__(self, storage_version, name, comment, esphome_version, src_version, arduino_version, address, esp_platform, board, build_path, firmware_bin_path, loaded_integrations): @@ -85,7 +83,7 @@ class StorageJSON(object): } def to_json(self): - return json.dumps(self.as_dict(), indent=2) + u'\n' + return json.dumps(self.as_dict(), indent=2) + '\n' def save(self, path): write_file_if_changed(path, self.to_json()) @@ -156,7 +154,7 @@ class StorageJSON(object): return isinstance(o, StorageJSON) and self.as_dict() == o.as_dict() -class EsphomeStorageJSON(object): +class EsphomeStorageJSON: def __init__(self, storage_version, cookie_secret, last_update_check, remote_version): # Version of the storage JSON schema @@ -189,7 +187,7 @@ class EsphomeStorageJSON(object): self.last_update_check_str = new.strftime("%Y-%m-%dT%H:%M:%S") def to_json(self): # type: () -> dict - return json.dumps(self.as_dict(), indent=2) + u'\n' + return json.dumps(self.as_dict(), indent=2) + '\n' def save(self, path): # type: (str) -> None write_file_if_changed(path, self.to_json()) @@ -216,7 +214,7 @@ class EsphomeStorageJSON(object): def get_default(): # type: () -> EsphomeStorageJSON return EsphomeStorageJSON( storage_version=1, - cookie_secret=text_type(binascii.hexlify(os.urandom(64))), + cookie_secret=binascii.hexlify(os.urandom(64)).decode(), last_update_check=None, remote_version=None, ) diff --git a/esphome/util.py b/esphome/util.py index b8e65cd576..6677946b01 100644 --- a/esphome/util.py +++ b/esphome/util.py @@ -1,5 +1,3 @@ -from __future__ import print_function - import collections import io import logging @@ -9,12 +7,11 @@ import subprocess import sys from esphome import const -from esphome.py_compat import IS_PY2, decode_text, text_type _LOGGER = logging.getLogger(__name__) -class RegistryEntry(object): +class RegistryEntry: def __init__(self, name, fun, type_id, schema): self.name = name self.fun = fun @@ -34,7 +31,7 @@ class RegistryEntry(object): class Registry(dict): def __init__(self, base_schema=None, type_id_key=None): - super(Registry, self).__init__() + super().__init__() self.base_schema = base_schema or {} self.type_id_key = type_id_key @@ -81,17 +78,17 @@ def safe_print(message=""): def shlex_quote(s): if not s: - return u"''" + return "''" if re.search(r'[^\w@%+=:,./-]', s) is None: return s - return u"'" + s.replace(u"'", u"'\"'\"'") + u"'" + return "'" + s.replace("'", "'\"'\"'") + "'" ANSI_ESCAPE = re.compile(r'\033[@-_][0-?]*[ -/]*[@-~]') -class RedirectText(object): +class RedirectText: def __init__(self, out, filter_lines=None): self._out = out if filter_lines is None: @@ -116,13 +113,12 @@ class RedirectText(object): self._out.write(s) def write(self, s): - # s is usually a text_type already (self._out is of type TextIOWrapper) + # s is usually a str already (self._out is of type TextIOWrapper) # However, s is sometimes also a bytes object in python3. Let's make sure it's a - # text_type + # str # If the conversion fails, we will create an exception, which is okay because we won't # be able to print it anyway. - text = decode_text(s) - assert isinstance(text, text_type) + text = s.decode() if self._filter_pattern is not None: self._line_buffer += text @@ -160,8 +156,8 @@ def run_external_command(func, *cmd, **kwargs): orig_argv = sys.argv orig_exit = sys.exit # mock sys.exit - full_cmd = u' '.join(shlex_quote(x) for x in cmd) - _LOGGER.info(u"Running: %s", full_cmd) + full_cmd = ' '.join(shlex_quote(x) for x in cmd) + _LOGGER.info("Running: %s", full_cmd) filter_lines = kwargs.get('filter_lines') orig_stdout = sys.stdout @@ -182,8 +178,8 @@ def run_external_command(func, *cmd, **kwargs): except SystemExit as err: return err.args[0] except Exception as err: # pylint: disable=broad-except - _LOGGER.error(u"Running command failed: %s", err) - _LOGGER.error(u"Please try running %s locally.", full_cmd) + _LOGGER.error("Running command failed: %s", err) + _LOGGER.error("Please try running %s locally.", full_cmd) return 1 finally: sys.argv = orig_argv @@ -198,8 +194,8 @@ def run_external_command(func, *cmd, **kwargs): def run_external_process(*cmd, **kwargs): - full_cmd = u' '.join(shlex_quote(x) for x in cmd) - _LOGGER.info(u"Running: %s", full_cmd) + full_cmd = ' '.join(shlex_quote(x) for x in cmd) + _LOGGER.info("Running: %s", full_cmd) filter_lines = kwargs.get('filter_lines') capture_stdout = kwargs.get('capture_stdout', False) @@ -215,8 +211,8 @@ def run_external_process(*cmd, **kwargs): stdout=sub_stdout, stderr=sub_stderr) except Exception as err: # pylint: disable=broad-except - _LOGGER.error(u"Running command failed: %s", err) - _LOGGER.error(u"Please try running %s locally.", full_cmd) + _LOGGER.error("Running command failed: %s", err) + _LOGGER.error("Please try running %s locally.", full_cmd) return 1 finally: if capture_stdout: @@ -233,29 +229,6 @@ class OrderedDict(collections.OrderedDict): def __repr__(self): return dict(self).__repr__() - def move_to_end(self, key, last=True): - if IS_PY2: - if len(self) == 1: - return - if last: - # When moving to end, just pop and re-add - val = self.pop(key) - self[key] = val - else: - # When moving to front, use internals here - # https://stackoverflow.com/a/16664932 - root = self._OrderedDict__root # pylint: disable=no-member - first = root[1] - link = self._OrderedDict__map[key] # pylint: disable=no-member - link_prev, link_next, _ = link - link_prev[1] = link_next - link_next[0] = link_prev - link[0] = root - link[1] = first - root[1] = first[0] = link - else: - super(OrderedDict, self).move_to_end(key, last=last) # pylint: disable=no-member - def list_yaml_files(folder): files = filter_yaml_files([os.path.join(folder, p) for p in os.listdir(folder)]) diff --git a/esphome/voluptuous_schema.py b/esphome/voluptuous_schema.py index 09fa1a6756..8193c1317c 100644 --- a/esphome/voluptuous_schema.py +++ b/esphome/voluptuous_schema.py @@ -3,8 +3,6 @@ import itertools import voluptuous as vol -from esphome.py_compat import string_types - class ExtraKeysInvalid(vol.Invalid): def __init__(self, *arg, **kwargs): @@ -22,14 +20,14 @@ def ensure_multiple_invalid(err): class _Schema(vol.Schema): """Custom cv.Schema that prints similar keys on error.""" def __init__(self, schema, required=False, extra=vol.PREVENT_EXTRA, extra_schemas=None): - super(_Schema, self).__init__(schema, required=required, extra=extra) + super().__init__(schema, required=required, extra=extra) # List of extra schemas to apply after validation # Should be used sparingly, as it's not a very voluptuous-way/clean way of # doing things. self._extra_schemas = extra_schemas or [] def __call__(self, data): - res = super(_Schema, self).__call__(data) + res = super().__call__(data) for extra in self._extra_schemas: try: res = extra(res) @@ -51,10 +49,10 @@ class _Schema(vol.Schema): raise ValueError("All schema keys must be wrapped in cv.Required or cv.Optional") # Keys that may be required - all_required_keys = set(key for key in schema if isinstance(key, vol.Required)) + all_required_keys = {key for key in schema if isinstance(key, vol.Required)} # Keys that may have defaults - all_default_keys = set(key for key in schema if isinstance(key, vol.Optional)) + all_default_keys = {key for key in schema if isinstance(key, vol.Optional)} # Recursively compile schema _compiled_schema = {} @@ -84,9 +82,9 @@ class _Schema(vol.Schema): key_names = [] for skey in schema: - if isinstance(skey, string_types): + if isinstance(skey, str): key_names.append(skey) - elif isinstance(skey, vol.Marker) and isinstance(skey.schema, string_types): + elif isinstance(skey, vol.Marker) and isinstance(skey.schema, str): key_names.append(skey.schema) def validate_mapping(path, iterable, out): @@ -156,7 +154,7 @@ class _Schema(vol.Schema): if self.extra == vol.ALLOW_EXTRA: out[key] = value elif self.extra != vol.REMOVE_EXTRA: - if isinstance(key, string_types) and key_names: + if isinstance(key, str) and key_names: matches = difflib.get_close_matches(key, key_names) errors.append(ExtraKeysInvalid('extra keys not allowed', key_path, candidates=matches)) @@ -195,5 +193,5 @@ class _Schema(vol.Schema): schema = schemas[0] if isinstance(schema, vol.Schema): schema = schema.schema - ret = super(_Schema, self).extend(schema, extra=extra) + ret = super().extend(schema, extra=extra) return _Schema(ret.schema, extra=ret.extra, extra_schemas=self._extra_schemas) diff --git a/esphome/vscode.py b/esphome/vscode.py index 6b35d4bd7a..e8c0b106f7 100644 --- a/esphome/vscode.py +++ b/esphome/vscode.py @@ -1,19 +1,17 @@ -from __future__ import print_function - import json import os +# pylint: disable=unused-import from esphome.config import load_config, _format_vol_invalid, Config from esphome.core import CORE, DocumentRange -from esphome.py_compat import text_type, safe_input +import esphome.config_validation as cv # pylint: disable=unused-import, wrong-import-order -import voluptuous as vol from typing import Optional def _get_invalid_range(res, invalid): - # type: (Config, vol.Invalid) -> Optional[DocumentRange] + # type: (Config, cv.Invalid) -> Optional[DocumentRange] return res.get_deepest_document_range_for_path(invalid.path) @@ -30,7 +28,7 @@ def _dump_range(range): } -class VSCodeResult(object): +class VSCodeResult: def __init__(self): self.yaml_errors = [] self.validation_errors = [] @@ -57,7 +55,7 @@ class VSCodeResult(object): def read_config(args): while True: CORE.reset() - data = json.loads(safe_input()) + data = json.loads(input()) assert data['type'] == 'validate' CORE.vscode = True CORE.ace = args.ace @@ -70,7 +68,7 @@ def read_config(args): try: res = load_config() except Exception as err: # pylint: disable=broad-except - vs.add_yaml_error(text_type(err)) + vs.add_yaml_error(str(err)) else: for err in res.errors: try: diff --git a/esphome/wizard.py b/esphome/wizard.py index 8cc759f934..b00f6d2b01 100644 --- a/esphome/wizard.py +++ b/esphome/wizard.py @@ -1,5 +1,3 @@ -from __future__ import print_function - import os import random import string @@ -11,7 +9,6 @@ import esphome.config_validation as cv from esphome.helpers import color, get_bool_env, write_file # pylint: disable=anomalous-backslash-in-string from esphome.pins import ESP32_BOARD_PINS, ESP8266_BOARD_PINS -from esphome.py_compat import safe_input, text_type from esphome.storage_json import StorageJSON, ext_storage_path from esphome.util import safe_print @@ -44,7 +41,7 @@ OTA_BIG = r""" ____ _______ \____/ |_/_/ \_\\ """ -BASE_CONFIG = u"""esphome: +BASE_CONFIG = """esphome: name: {name} platform: {platform} board: {board} @@ -75,7 +72,7 @@ def sanitize_double_quotes(value): def wizard_file(**kwargs): letters = string.ascii_letters + string.digits ap_name_base = kwargs['name'].replace('_', ' ').title() - ap_name = "{} Fallback Hotspot".format(ap_name_base) + ap_name = f"{ap_name_base} Fallback Hotspot" if len(ap_name) > 32: ap_name = ap_name_base kwargs['fallback_name'] = ap_name @@ -84,9 +81,9 @@ def wizard_file(**kwargs): config = BASE_CONFIG.format(**kwargs) if kwargs['password']: - config += u' password: "{0}"\n\nota:\n password: "{0}"\n'.format(kwargs['password']) + config += ' password: "{0}"\n\nota:\n password: "{0}"\n'.format(kwargs['password']) else: - config += u"\nota:\n" + config += "\nota:\n" return config @@ -119,7 +116,7 @@ else: def safe_print_step(step, big): safe_print() safe_print() - safe_print("============= STEP {} =============".format(step)) + safe_print(f"============= STEP {step} =============") safe_print(big) safe_print("===================================") sleep(0.25) @@ -127,24 +124,24 @@ def safe_print_step(step, big): def default_input(text, default): safe_print() - safe_print(u"Press ENTER for default ({})".format(default)) - return safe_input(text.format(default)) or default + safe_print(f"Press ENTER for default ({default})") + return input(text.format(default)) or default # From https://stackoverflow.com/a/518232/8924614 def strip_accents(value): - return u''.join(c for c in unicodedata.normalize('NFD', text_type(value)) - if unicodedata.category(c) != 'Mn') + return ''.join(c for c in unicodedata.normalize('NFD', str(value)) + if unicodedata.category(c) != 'Mn') def wizard(path): if not path.endswith('.yaml') and not path.endswith('.yml'): - safe_print(u"Please make your configuration file {} have the extension .yaml or .yml" - u"".format(color('cyan', path))) + safe_print("Please make your configuration file {} have the extension .yaml or .yml" + "".format(color('cyan', path))) return 1 if os.path.exists(path): - safe_print(u"Uh oh, it seems like {} already exists, please delete that file first " - u"or chose another configuration file.".format(color('cyan', path))) + safe_print("Uh oh, it seems like {} already exists, please delete that file first " + "or chose another configuration file.".format(color('cyan', path))) return 1 safe_print("Hi there!") sleep(1.5) @@ -164,21 +161,21 @@ def wizard(path): color('bold_white', "livingroom"))) safe_print() sleep(1) - name = safe_input(color("bold_white", "(name): ")) + name = input(color("bold_white", "(name): ")) while True: try: name = cv.valid_name(name) break except vol.Invalid: - safe_print(color("red", u"Oh noes, \"{}\" isn't a valid name. Names can only include " - u"numbers, letters and underscores.".format(name))) + safe_print(color("red", "Oh noes, \"{}\" isn't a valid name. Names can only include " + "numbers, letters and underscores.".format(name))) name = strip_accents(name).replace(' ', '_') - name = u''.join(c for c in name if c in cv.ALLOWED_NAME_CHARS) - safe_print(u"Shall I use \"{}\" as the name instead?".format(color('cyan', name))) + name = ''.join(c for c in name if c in cv.ALLOWED_NAME_CHARS) + safe_print("Shall I use \"{}\" as the name instead?".format(color('cyan', name))) sleep(0.5) - name = default_input(u"(name [{}]): ", name) + name = default_input("(name [{}]): ", name) - safe_print(u"Great! Your node is now called \"{}\".".format(color('cyan', name))) + safe_print("Great! Your node is now called \"{}\".".format(color('cyan', name))) sleep(1) safe_print_step(2, ESP_BIG) safe_print("Now I'd like to know what microcontroller you're using so that I can compile " @@ -189,14 +186,14 @@ def wizard(path): sleep(0.5) safe_print() safe_print("Please enter either ESP32 or ESP8266.") - platform = safe_input(color("bold_white", "(ESP32/ESP8266): ")) + platform = input(color("bold_white", "(ESP32/ESP8266): ")) try: platform = vol.All(vol.Upper, vol.Any('ESP32', 'ESP8266'))(platform) break except vol.Invalid: - safe_print(u"Unfortunately, I can't find an espressif microcontroller called " - u"\"{}\". Please try again.".format(platform)) - safe_print(u"Thanks! You've chosen {} as your platform.".format(color('cyan', platform))) + safe_print("Unfortunately, I can't find an espressif microcontroller called " + "\"{}\". Please try again.".format(platform)) + safe_print("Thanks! You've chosen {} as your platform.".format(color('cyan', platform))) safe_print() sleep(1) @@ -221,17 +218,17 @@ def wizard(path): safe_print("Options: {}".format(', '.join(sorted(boards)))) while True: - board = safe_input(color("bold_white", "(board): ")) + board = input(color("bold_white", "(board): ")) try: board = vol.All(vol.Lower, vol.Any(*boards))(board) break except vol.Invalid: - safe_print(color('red', "Sorry, I don't think the board \"{}\" exists.".format(board))) + safe_print(color('red', f"Sorry, I don't think the board \"{board}\" exists.")) safe_print() sleep(0.25) safe_print() - safe_print(u"Way to go! You've chosen {} as your board.".format(color('cyan', board))) + safe_print("Way to go! You've chosen {} as your board.".format(color('cyan', board))) safe_print() sleep(1) @@ -241,22 +238,22 @@ def wizard(path): safe_print() sleep(1) safe_print("First, what's the " + color('green', 'SSID') + - u" (the name) of the WiFi network {} I should connect to?".format(name)) + f" (the name) of the WiFi network {name} I should connect to?") sleep(1.5) safe_print("For example \"{}\".".format(color('bold_white', "Abraham Linksys"))) while True: - ssid = safe_input(color('bold_white', "(ssid): ")) + ssid = input(color('bold_white', "(ssid): ")) try: ssid = cv.ssid(ssid) break except vol.Invalid: - safe_print(color('red', u"Unfortunately, \"{}\" doesn't seem to be a valid SSID. " - u"Please try again.".format(ssid))) + safe_print(color('red', "Unfortunately, \"{}\" doesn't seem to be a valid SSID. " + "Please try again.".format(ssid))) safe_print() sleep(1) - safe_print(u"Thank you very much! You've just chosen \"{}\" as your SSID." - u"".format(color('cyan', ssid))) + safe_print("Thank you very much! You've just chosen \"{}\" as your SSID." + "".format(color('cyan', ssid))) safe_print() sleep(0.75) @@ -265,7 +262,7 @@ def wizard(path): safe_print() safe_print("For example \"{}\"".format(color('bold_white', 'PASSWORD42'))) sleep(0.5) - psk = safe_input(color('bold_white', '(PSK): ')) + psk = input(color('bold_white', '(PSK): ')) safe_print("Perfect! WiFi is now set up (you can create static IPs and so on later).") sleep(1.5) @@ -277,7 +274,7 @@ def wizard(path): safe_print() sleep(0.25) safe_print("Press ENTER for no password") - password = safe_input(color('bold_white', '(password): ')) + password = input(color('bold_white', '(password): ')) wizard_write(path=path, name=name, platform=platform, board=board, ssid=ssid, psk=psk, password=password) diff --git a/esphome/writer.py b/esphome/writer.py index b036413a66..b3a60c5de9 100644 --- a/esphome/writer.py +++ b/esphome/writer.py @@ -1,5 +1,3 @@ -from __future__ import print_function - import logging import os import re @@ -14,19 +12,19 @@ from esphome.storage_json import StorageJSON, storage_path _LOGGER = logging.getLogger(__name__) -CPP_AUTO_GENERATE_BEGIN = u'// ========== AUTO GENERATED CODE BEGIN ===========' -CPP_AUTO_GENERATE_END = u'// =========== AUTO GENERATED CODE END ============' -CPP_INCLUDE_BEGIN = u'// ========== AUTO GENERATED INCLUDE BLOCK BEGIN ===========' -CPP_INCLUDE_END = u'// ========== AUTO GENERATED INCLUDE BLOCK END ===========' -INI_AUTO_GENERATE_BEGIN = u'; ========== AUTO GENERATED CODE BEGIN ===========' -INI_AUTO_GENERATE_END = u'; =========== AUTO GENERATED CODE END ============' +CPP_AUTO_GENERATE_BEGIN = '// ========== AUTO GENERATED CODE BEGIN ===========' +CPP_AUTO_GENERATE_END = '// =========== AUTO GENERATED CODE END ============' +CPP_INCLUDE_BEGIN = '// ========== AUTO GENERATED INCLUDE BLOCK BEGIN ===========' +CPP_INCLUDE_END = '// ========== AUTO GENERATED INCLUDE BLOCK END ===========' +INI_AUTO_GENERATE_BEGIN = '; ========== AUTO GENERATED CODE BEGIN ===========' +INI_AUTO_GENERATE_END = '; =========== AUTO GENERATED CODE END ============' -CPP_BASE_FORMAT = (u"""// Auto generated code by esphome -""", u"""" +CPP_BASE_FORMAT = ("""// Auto generated code by esphome +""", """" void setup() { // ===== DO NOT EDIT ANYTHING BELOW THIS LINE ===== - """, u""" + """, """ // ========= YOU CAN EDIT AFTER THIS LINE ========= App.setup(); } @@ -36,7 +34,7 @@ void loop() { } """) -INI_BASE_FORMAT = (u"""; Auto generated code by esphome +INI_BASE_FORMAT = ("""; Auto generated code by esphome [common] lib_deps = @@ -44,7 +42,7 @@ build_flags = upload_flags = ; ===== DO NOT EDIT ANYTHING BELOW THIS LINE ===== -""", u""" +""", """ ; ========= YOU CAN EDIT AFTER THIS LINE ========= """) @@ -62,8 +60,8 @@ def get_flags(key): def get_include_text(): - include_text = u'#include "esphome.h"\n' \ - u'using namespace esphome;\n' + include_text = '#include "esphome.h"\n' \ + 'using namespace esphome;\n' for _, component, conf in iter_components(CORE.config): if not hasattr(component, 'includes'): continue @@ -106,7 +104,7 @@ def migrate_src_version_0_to_1(): if CPP_INCLUDE_BEGIN not in content: content, count = replace_file_content(content, r'#include "esphomelib/application.h"', - CPP_INCLUDE_BEGIN + u'\n' + CPP_INCLUDE_END) + CPP_INCLUDE_BEGIN + '\n' + CPP_INCLUDE_END) if count == 0: _LOGGER.error("Migration failed. ESPHome 1.10.0 needs to have a new auto-generated " "include section in the %s file. Please remove %s and let it be " @@ -160,14 +158,14 @@ def update_storage_json(): def format_ini(data): - content = u'' + content = '' for key, value in sorted(data.items()): if isinstance(value, (list, set, tuple)): - content += u'{} =\n'.format(key) + content += f'{key} =\n' for x in value: - content += u' {}\n'.format(x) + content += f' {x}\n' else: - content += u'{} = {}\n'.format(key, value) + content += f'{key} = {value}\n' return content @@ -216,7 +214,7 @@ def get_ini_content(): # data['lib_ldf_mode'] = 'chain' data.update(CORE.config[CONF_ESPHOME].get(CONF_PLATFORMIO_OPTIONS, {})) - content = u'[env:{}]\n'.format(CORE.name) + content = f'[env:{CORE.name}]\n' content += format_ini(data) return content @@ -225,18 +223,18 @@ def get_ini_content(): def find_begin_end(text, begin_s, end_s): begin_index = text.find(begin_s) if begin_index == -1: - raise EsphomeError(u"Could not find auto generated code begin in file, either " - u"delete the main sketch file or insert the comment again.") + raise EsphomeError("Could not find auto generated code begin in file, either " + "delete the main sketch file or insert the comment again.") if text.find(begin_s, begin_index + 1) != -1: - raise EsphomeError(u"Found multiple auto generate code begins, don't know " - u"which to chose, please remove one of them.") + raise EsphomeError("Found multiple auto generate code begins, don't know " + "which to chose, please remove one of them.") end_index = text.find(end_s) if end_index == -1: - raise EsphomeError(u"Could not find auto generated code end in file, either " - u"delete the main sketch file or insert the comment again.") + raise EsphomeError("Could not find auto generated code end in file, either " + "delete the main sketch file or insert the comment again.") if text.find(end_s, end_index + 1) != -1: - raise EsphomeError(u"Found multiple auto generate code endings, don't know " - u"which to chose, please remove one of them.") + raise EsphomeError("Found multiple auto generate code endings, don't know " + "which to chose, please remove one of them.") return text[:begin_index], text[(end_index + len(end_s)):] @@ -263,17 +261,17 @@ def write_platformio_project(): write_platformio_ini(content) -DEFINES_H_FORMAT = ESPHOME_H_FORMAT = u"""\ +DEFINES_H_FORMAT = ESPHOME_H_FORMAT = """\ #pragma once {} """ -VERSION_H_FORMAT = u"""\ +VERSION_H_FORMAT = """\ #pragma once #define ESPHOME_VERSION "{}" """ DEFINES_H_TARGET = 'esphome/core/defines.h' VERSION_H_TARGET = 'esphome/core/version.h' -ESPHOME_README_TXT = u""" +ESPHOME_README_TXT = """ THIS DIRECTORY IS AUTO-GENERATED, DO NOT MODIFY ESPHome automatically populates the esphome/ directory, and any @@ -298,9 +296,9 @@ def copy_src_tree(): include_l = [] for target, path in source_files_l: if os.path.splitext(path)[1] in HEADER_FILE_EXTENSIONS: - include_l.append(u'#include "{}"'.format(target)) - include_l.append(u'') - include_s = u'\n'.join(include_l) + include_l.append(f'#include "{target}"') + include_l.append('') + include_s = '\n'.join(include_l) source_files_copy = source_files.copy() source_files_copy.pop(DEFINES_H_TARGET) @@ -340,7 +338,7 @@ def copy_src_tree(): def generate_defines_h(): define_content_l = [x.as_macro for x in CORE.defines] define_content_l.sort() - return DEFINES_H_FORMAT.format(u'\n'.join(define_content_l)) + return DEFINES_H_FORMAT.format('\n'.join(define_content_l)) def write_cpp(code_s): @@ -354,11 +352,11 @@ def write_cpp(code_s): code_format = CPP_BASE_FORMAT copy_src_tree() - global_s = u'#include "esphome.h"\n' + global_s = '#include "esphome.h"\n' global_s += CORE.cpp_global_section - full_file = code_format[0] + CPP_INCLUDE_BEGIN + u'\n' + global_s + CPP_INCLUDE_END - full_file += code_format[1] + CPP_AUTO_GENERATE_BEGIN + u'\n' + code_s + CPP_AUTO_GENERATE_END + full_file = code_format[0] + CPP_INCLUDE_BEGIN + '\n' + global_s + CPP_INCLUDE_END + full_file += code_format[1] + CPP_AUTO_GENERATE_BEGIN + '\n' + code_s + CPP_AUTO_GENERATE_END full_file += code_format[2] write_file_if_changed(path, full_file) diff --git a/esphome/yaml_util.py b/esphome/yaml_util.py index 0e5b4593e9..053fba6274 100644 --- a/esphome/yaml_util.py +++ b/esphome/yaml_util.py @@ -1,5 +1,3 @@ -from __future__ import print_function - import fnmatch import functools import inspect @@ -15,7 +13,6 @@ from esphome import core from esphome.config_helpers import read_config_file from esphome.core import EsphomeError, IPAddress, Lambda, MACAddress, TimePeriod, DocumentRange from esphome.helpers import add_class_to_obj -from esphome.py_compat import text_type, IS_PY2 from esphome.util import OrderedDict, filter_yaml_files _LOGGER = logging.getLogger(__name__) @@ -23,12 +20,12 @@ _LOGGER = logging.getLogger(__name__) # Mostly copied from Home Assistant because that code works fine and # let's not reinvent the wheel here -SECRET_YAML = u'secrets.yaml' +SECRET_YAML = 'secrets.yaml' _SECRET_CACHE = {} _SECRET_VALUES = {} -class ESPHomeDataBase(object): +class ESPHomeDataBase: @property def esp_range(self): return getattr(self, '_esp_range', None) @@ -38,7 +35,7 @@ class ESPHomeDataBase(object): self._esp_range = DocumentRange.from_marks(node.start_mark, node.end_mark) -class ESPForceValue(object): +class ESPForceValue: pass @@ -74,27 +71,27 @@ class ESPHomeLoader(yaml.SafeLoader): # pylint: disable=too-many-ancestors @_add_data_ref def construct_yaml_int(self, node): - return super(ESPHomeLoader, self).construct_yaml_int(node) + return super().construct_yaml_int(node) @_add_data_ref def construct_yaml_float(self, node): - return super(ESPHomeLoader, self).construct_yaml_float(node) + return super().construct_yaml_float(node) @_add_data_ref def construct_yaml_binary(self, node): - return super(ESPHomeLoader, self).construct_yaml_binary(node) + return super().construct_yaml_binary(node) @_add_data_ref def construct_yaml_omap(self, node): - return super(ESPHomeLoader, self).construct_yaml_omap(node) + return super().construct_yaml_omap(node) @_add_data_ref def construct_yaml_str(self, node): - return super(ESPHomeLoader, self).construct_yaml_str(node) + return super().construct_yaml_str(node) @_add_data_ref def construct_yaml_seq(self, node): - return super(ESPHomeLoader, self).construct_yaml_seq(node) + return super().construct_yaml_seq(node) @_add_data_ref def construct_yaml_map(self, node): @@ -130,12 +127,12 @@ class ESPHomeLoader(yaml.SafeLoader): # pylint: disable=too-many-ancestors hash(key) except TypeError: raise yaml.constructor.ConstructorError( - 'Invalid key "{}" (not hashable)'.format(key), key_node.start_mark) + f'Invalid key "{key}" (not hashable)', key_node.start_mark) # Check if it is a duplicate key if key in seen_keys: raise yaml.constructor.ConstructorError( - 'Duplicate key "{}"'.format(key), key_node.start_mark, + f'Duplicate key "{key}"', key_node.start_mark, 'NOTE: Previous declaration here:', seen_keys[key], ) seen_keys[key] = key_node.start_mark @@ -194,11 +191,11 @@ class ESPHomeLoader(yaml.SafeLoader): # pylint: disable=too-many-ancestors args = node.value.split() # Check for a default value if len(args) > 1: - return os.getenv(args[0], u' '.join(args[1:])) + return os.getenv(args[0], ' '.join(args[1:])) if args[0] in os.environ: return os.environ[args[0]] raise yaml.MarkedYAMLError( - u"Environment variable '{}' not defined".format(node.value), node.start_mark + f"Environment variable '{node.value}' not defined", node.start_mark ) @property @@ -213,10 +210,10 @@ class ESPHomeLoader(yaml.SafeLoader): # pylint: disable=too-many-ancestors secrets = _load_yaml_internal(self._rel_path(SECRET_YAML)) if node.value not in secrets: raise yaml.MarkedYAMLError( - u"Secret '{}' not defined".format(node.value), node.start_mark + f"Secret '{node.value}' not defined", node.start_mark ) val = secrets[node.value] - _SECRET_VALUES[text_type(val)] = node.value + _SECRET_VALUES[str(val)] = node.value return val @_add_data_ref @@ -259,7 +256,7 @@ class ESPHomeLoader(yaml.SafeLoader): # pylint: disable=too-many-ancestors @_add_data_ref def construct_lambda(self, node): - return Lambda(text_type(node.value)) + return Lambda(str(node.value)) @_add_data_ref def construct_force(self, node): @@ -267,13 +264,13 @@ class ESPHomeLoader(yaml.SafeLoader): # pylint: disable=too-many-ancestors return add_class_to_obj(obj, ESPForceValue) -ESPHomeLoader.add_constructor(u'tag:yaml.org,2002:int', ESPHomeLoader.construct_yaml_int) -ESPHomeLoader.add_constructor(u'tag:yaml.org,2002:float', ESPHomeLoader.construct_yaml_float) -ESPHomeLoader.add_constructor(u'tag:yaml.org,2002:binary', ESPHomeLoader.construct_yaml_binary) -ESPHomeLoader.add_constructor(u'tag:yaml.org,2002:omap', ESPHomeLoader.construct_yaml_omap) -ESPHomeLoader.add_constructor(u'tag:yaml.org,2002:str', ESPHomeLoader.construct_yaml_str) -ESPHomeLoader.add_constructor(u'tag:yaml.org,2002:seq', ESPHomeLoader.construct_yaml_seq) -ESPHomeLoader.add_constructor(u'tag:yaml.org,2002:map', ESPHomeLoader.construct_yaml_map) +ESPHomeLoader.add_constructor('tag:yaml.org,2002:int', ESPHomeLoader.construct_yaml_int) +ESPHomeLoader.add_constructor('tag:yaml.org,2002:float', ESPHomeLoader.construct_yaml_float) +ESPHomeLoader.add_constructor('tag:yaml.org,2002:binary', ESPHomeLoader.construct_yaml_binary) +ESPHomeLoader.add_constructor('tag:yaml.org,2002:omap', ESPHomeLoader.construct_yaml_omap) +ESPHomeLoader.add_constructor('tag:yaml.org,2002:str', ESPHomeLoader.construct_yaml_str) +ESPHomeLoader.add_constructor('tag:yaml.org,2002:seq', ESPHomeLoader.construct_yaml_seq) +ESPHomeLoader.add_constructor('tag:yaml.org,2002:map', ESPHomeLoader.construct_yaml_map) ESPHomeLoader.add_constructor('!env_var', ESPHomeLoader.construct_env_var) ESPHomeLoader.add_constructor('!secret', ESPHomeLoader.construct_secret) ESPHomeLoader.add_constructor('!include', ESPHomeLoader.construct_include) @@ -313,7 +310,7 @@ def dump(dict_): def _is_file_valid(name): """Decide if a file is valid.""" - return not name.startswith(u'.') + return not name.startswith('.') def _find_files(directory, pattern): @@ -328,7 +325,7 @@ def _find_files(directory, pattern): def is_secret(value): try: - return _SECRET_VALUES[text_type(value)] + return _SECRET_VALUES[str(value)] except (KeyError, ValueError): return None @@ -358,31 +355,31 @@ class ESPHomeDumper(yaml.SafeDumper): # pylint: disable=too-many-ancestors return node def represent_secret(self, value): - return self.represent_scalar(tag=u'!secret', value=_SECRET_VALUES[text_type(value)]) + return self.represent_scalar(tag='!secret', value=_SECRET_VALUES[str(value)]) def represent_stringify(self, value): if is_secret(value): return self.represent_secret(value) - return self.represent_scalar(tag=u'tag:yaml.org,2002:str', value=text_type(value)) + return self.represent_scalar(tag='tag:yaml.org,2002:str', value=str(value)) # pylint: disable=arguments-differ def represent_bool(self, value): - return self.represent_scalar(u'tag:yaml.org,2002:bool', u'true' if value else u'false') + return self.represent_scalar('tag:yaml.org,2002:bool', 'true' if value else 'false') def represent_int(self, value): if is_secret(value): return self.represent_secret(value) - return self.represent_scalar(tag=u'tag:yaml.org,2002:int', value=text_type(value)) + return self.represent_scalar(tag='tag:yaml.org,2002:int', value=str(value)) def represent_float(self, value): if is_secret(value): return self.represent_secret(value) if math.isnan(value): - value = u'.nan' + value = '.nan' elif math.isinf(value): - value = u'.inf' if value > 0 else u'-.inf' + value = '.inf' if value > 0 else '-.inf' else: - value = text_type(repr(value)).lower() + value = str(repr(value)).lower() # Note that in some cases `repr(data)` represents a float number # without the decimal parts. For instance: # >>> repr(1e17) @@ -390,9 +387,9 @@ class ESPHomeDumper(yaml.SafeDumper): # pylint: disable=too-many-ancestors # Unfortunately, this is not a valid float representation according # to the definition of the `!!float` tag. We fix this by adding # '.0' before the 'e' symbol. - if u'.' not in value and u'e' in value: - value = value.replace(u'e', u'.0e', 1) - return self.represent_scalar(tag=u'tag:yaml.org,2002:float', value=value) + if '.' not in value and 'e' in value: + value = value.replace('e', '.0e', 1) + return self.represent_scalar(tag='tag:yaml.org,2002:float', value=value) def represent_lambda(self, value): if is_secret(value.value): @@ -417,9 +414,6 @@ ESPHomeDumper.add_multi_representer(bool, ESPHomeDumper.represent_bool) ESPHomeDumper.add_multi_representer(str, ESPHomeDumper.represent_stringify) ESPHomeDumper.add_multi_representer(int, ESPHomeDumper.represent_int) ESPHomeDumper.add_multi_representer(float, ESPHomeDumper.represent_float) -if IS_PY2: - ESPHomeDumper.add_multi_representer(unicode, ESPHomeDumper.represent_stringify) - ESPHomeDumper.add_multi_representer(long, ESPHomeDumper.represent_int) ESPHomeDumper.add_multi_representer(IPAddress, ESPHomeDumper.represent_stringify) ESPHomeDumper.add_multi_representer(MACAddress, ESPHomeDumper.represent_stringify) ESPHomeDumper.add_multi_representer(TimePeriod, ESPHomeDumper.represent_stringify) diff --git a/esphome/zeroconf.py b/esphome/zeroconf.py index 9c8f8eab77..a8ca5b3c53 100644 --- a/esphome/zeroconf.py +++ b/esphome/zeroconf.py @@ -12,8 +12,6 @@ import time import ifaddr -from esphome.py_compat import indexbytes, text_type - log = logging.getLogger(__name__) # Some timing constants @@ -83,7 +81,7 @@ class IncomingDecodeError(Error): # pylint: disable=no-init -class QuietLogger(object): +class QuietLogger: _seen_logs = {} @classmethod @@ -112,7 +110,7 @@ class QuietLogger(object): logger(*args) -class DNSEntry(object): +class DNSEntry: """A DNS entry""" def __init__(self, name, type_, class_): @@ -281,7 +279,7 @@ class DNSIncoming(QuietLogger): def read_utf(self, offset, length): """Reads a UTF-8 string of a given length from the packet""" - return text_type(self.data[offset:offset + length], 'utf-8', 'replace') + return str(self.data[offset:offset + length], 'utf-8', 'replace') def read_name(self): """Reads a domain name from the packet""" @@ -291,7 +289,7 @@ class DNSIncoming(QuietLogger): first = off while True: - length = indexbytes(self.data, off) + length = self.data[off] off += 1 if length == 0: break @@ -302,13 +300,13 @@ class DNSIncoming(QuietLogger): elif t == 0xC0: if next_ < 0: next_ = off + 1 - off = ((length & 0x3F) << 8) | indexbytes(self.data, off) + off = ((length & 0x3F) << 8) | self.data[off] if off >= first: raise IncomingDecodeError( - "Bad domain name (circular) at %s" % (off,)) + f"Bad domain name (circular) at {off}") first = off else: - raise IncomingDecodeError("Bad domain name at %s" % (off,)) + raise IncomingDecodeError(f"Bad domain name at {off}") if next_ >= 0: self.offset = next_ @@ -318,7 +316,7 @@ class DNSIncoming(QuietLogger): return result -class DNSOutgoing(object): +class DNSOutgoing: """Object representation of an outgoing packet""" def __init__(self, flags): @@ -461,7 +459,7 @@ class Engine(threading.Thread): if reader: reader.handle_read(socket_) - except (select.error, socket.error) as e: + except OSError as e: # If the socket was closed by another thread, during # shutdown, ignore it and exit if e.args[0] != socket.EBADF or not self.zc.done: @@ -500,7 +498,7 @@ class Listener(QuietLogger): self.zc.handle_response(msg) -class RecordUpdateListener(object): +class RecordUpdateListener: def update_record(self, zc, now, record): raise NotImplementedError() @@ -578,7 +576,7 @@ class DashboardStatus(RecordUpdateListener, threading.Thread): self.on_update({key: self.host_status(key) for key in self.key_to_host}) def request_query(self, hosts): - self.query_hosts = set(host for host in hosts.values()) + self.query_hosts = set(hosts.values()) self.key_to_host = hosts self.query_event.set() @@ -605,12 +603,12 @@ class DashboardStatus(RecordUpdateListener, threading.Thread): def get_all_addresses(): - return list(set( + return list({ addr.ip for iface in ifaddr.get_adapters() for addr in iface.ips if addr.is_IPv4 and addr.network_prefix != 32 # Host only netmask 255.255.255.255 - )) + }) def new_socket(): @@ -631,7 +629,7 @@ def new_socket(): else: try: s.setsockopt(socket.SOL_SOCKET, reuseport, 1) - except (OSError, socket.error) as err: + except OSError as err: # OSError on python 3, socket.error on python 2 if err.errno != errno.ENOPROTOOPT: raise @@ -662,7 +660,7 @@ class Zeroconf(QuietLogger): _value = socket.inet_aton(_MDNS_ADDR) + socket.inet_aton(i) self._listen_socket.setsockopt( socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, _value) - except socket.error as e: + except OSError as e: _errno = e.args[0] if _errno == errno.EADDRINUSE: log.info( diff --git a/pylintrc b/pylintrc index 89cc73656f..c65a9a7cd9 100644 --- a/pylintrc +++ b/pylintrc @@ -25,9 +25,3 @@ disable= stop-iteration-return, no-self-use, import-outside-toplevel, - - -additional-builtins= - unicode, - long, - raw_input diff --git a/requirements.txt b/requirements.txt index b8af2a605b..8c763ef9a6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,6 @@ PyYAML==5.2 paho-mqtt==1.5.0 colorlog==4.0.2 tornado==5.1.1 -typing>=3.6.6;python_version<"3.5" protobuf==3.11.1 tzlocal==2.0.0 pytz==2019.3 diff --git a/requirements_test.txt b/requirements_test.txt index ff02badf80..7711b3867a 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -3,7 +3,6 @@ PyYAML==5.2 paho-mqtt==1.5.0 colorlog==4.0.2 tornado==5.1.1 -typing>=3.6.6;python_version<"3.5" protobuf==3.11.1 tzlocal==2.0.0 pytz==2019.3 diff --git a/script/api_protobuf/api_options_pb2.py b/script/api_protobuf/api_options_pb2.py index 52cbbde678..e690a2c5d7 100644 --- a/script/api_protobuf/api_options_pb2.py +++ b/script/api_protobuf/api_options_pb2.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! # source: api_options.proto diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 8373e0bd66..2ecaec10bd 100644 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -39,12 +39,12 @@ content = prot.read_bytes() d = descriptor.FileDescriptorSet.FromString(content) -def indent_list(text, padding=u' '): +def indent_list(text, padding=' '): return [padding + line for line in text.splitlines()] -def indent(text, padding=u' '): - return u'\n'.join(indent_list(text, padding)) +def indent(text, padding=' '): + return '\n'.join(indent_list(text, padding)) def camel_to_snake(name): @@ -432,7 +432,7 @@ class SInt64Type(TypeInfo): class RepeatedTypeInfo(TypeInfo): def __init__(self, field): - super(RepeatedTypeInfo, self).__init__(field) + super().__init__(field) self._ti = TYPE_INFO[field.type](field) @property diff --git a/script/build_compile_commands.py b/script/build_compile_commands.py index 31e4c2fa56..f0fc48ad98 100755 --- a/script/build_compile_commands.py +++ b/script/build_compile_commands.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 import sys import os.path diff --git a/script/ci-custom.py b/script/ci-custom.py index 51b7b9d9b5..b2b838cb5b 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -1,5 +1,4 @@ -#!/usr/bin/env python -from __future__ import print_function +#!/usr/bin/env python3 import codecs import collections @@ -105,7 +104,7 @@ def lint_re_check(regex, **kwargs): err = func(fname, match) if err is None: continue - errors.append("{} See line {}.".format(err, lineno)) + errors.append(f"{err} See line {lineno}.") return errors return decor(new_func) return decorator @@ -134,7 +133,7 @@ def lint_ino(fname): return "This file extension (.ino) is not allowed. Please use either .cpp or .h" -@lint_file_check(exclude=['*{}'.format(f) for f in file_types] + [ +@lint_file_check(exclude=[f'*{f}' for f in file_types] + [ '.clang-*', '.dockerignore', '.editorconfig', '*.gitignore', 'LICENSE', 'pylintrc', 'MANIFEST.in', 'docker/Dockerfile*', 'docker/rootfs/*', 'script/*', ]) @@ -177,7 +176,7 @@ CPP_RE_EOL = r'\s*?(?://.*?)?$' def highlight(s): - return '\033[36m{}\033[0m'.format(s) + return f'\033[36m{s}\033[0m' @lint_re_check(r'^#define\s+([a-zA-Z0-9_]+)\s+([0-9bx]+)' + CPP_RE_EOL, @@ -268,7 +267,7 @@ def lint_constants_usage(): def relative_cpp_search_text(fname, content): parts = fname.split('/') integration = parts[2] - return '#include "esphome/components/{}'.format(integration) + return f'#include "esphome/components/{integration}' @lint_content_find_check(relative_cpp_search_text, include=['esphome/components/*.cpp']) @@ -284,7 +283,7 @@ def lint_relative_cpp_import(fname): def relative_py_search_text(fname, content): parts = fname.split('/') integration = parts[2] - return 'esphome.components.{}'.format(integration) + return f'esphome.components.{integration}' @lint_content_find_check(relative_py_search_text, include=['esphome/components/*.py'], @@ -303,7 +302,7 @@ def lint_relative_py_import(fname): def lint_namespace(fname, content): expected_name = re.match(r'^esphome/components/([^/]+)/.*', fname.replace(os.path.sep, '/')).group(1) - search = 'namespace {}'.format(expected_name) + search = f'namespace {expected_name}' if search in content: return None return 'Invalid namespace found in C++ file. All integration C++ files should put all ' \ @@ -380,7 +379,7 @@ for fname in files: run_checks(LINT_POST_CHECKS, 'POST') for f, errs in sorted(errors.items()): - print("\033[0;32m************* File \033[1;32m{}\033[0m".format(f)) + print(f"\033[0;32m************* File \033[1;32m{f}\033[0m") for err in errs: print(err) print() diff --git a/script/clang-format b/script/clang-format index 89a5acd746..e9c3692bb8 100755 --- a/script/clang-format +++ b/script/clang-format @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 from __future__ import print_function diff --git a/script/clang-tidy b/script/clang-tidy index f178e036b1..1005d15580 100755 --- a/script/clang-tidy +++ b/script/clang-tidy @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 from __future__ import print_function diff --git a/script/helpers.py b/script/helpers.py index 243dfde49e..c9bf5224b1 100644 --- a/script/helpers.py +++ b/script/helpers.py @@ -12,11 +12,11 @@ temp_header_file = os.path.join(root_path, '.temp-clang-tidy.cpp') def shlex_quote(s): if not s: - return u"''" + return "''" if re.search(r'[^\w@%+=:,./-]', s) is None: return s - return u"'" + s.replace(u"'", u"'\"'\"'") + u"'" + return "'" + s.replace("'", "'\"'\"'") + "'" def build_all_include(): @@ -29,7 +29,7 @@ def build_all_include(): if ext in filetypes: path = os.path.relpath(path, root_path) include_p = path.replace(os.path.sep, '/') - headers.append('#include "{}"'.format(include_p)) + headers.append(f'#include "{include_p}"') headers.sort() headers.append('') content = '\n'.join(headers) @@ -47,7 +47,7 @@ def build_compile_commands(): gcc_flags = json.load(f) exec_path = gcc_flags['execPath'] include_paths = gcc_flags['gccIncludePaths'].split(',') - includes = ['-I{}'.format(p) for p in include_paths] + includes = [f'-I{p}' for p in include_paths] cpp_flags = gcc_flags['gccDefaultCppFlags'].split(' ') defines = [flag for flag in cpp_flags if flag.startswith('-D')] command = [exec_path] @@ -102,7 +102,7 @@ def splitlines_no_ends(string): def changed_files(): for remote in ('upstream', 'origin'): - command = ['git', 'merge-base', '{}/dev'.format(remote), 'HEAD'] + command = ['git', 'merge-base', f'{remote}/dev', 'HEAD'] try: merge_base = splitlines_no_ends(get_output(*command))[0] break @@ -124,7 +124,7 @@ def filter_changed(files): if not files: print(" No changed files!") for c in files: - print(" {}".format(c)) + print(f" {c}") return files diff --git a/script/lint-python b/script/lint-python index 3fbc329ab0..4915115262 100755 --- a/script/lint-python +++ b/script/lint-python @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 from __future__ import print_function @@ -61,7 +61,7 @@ def main(): continue file_ = line[0] linno = line[1] - msg = (u':'.join(line[3:])).strip() + msg = (':'.join(line[3:])).strip() print_error(file_, linno, msg) errors += 1 @@ -74,7 +74,7 @@ def main(): continue file_ = line[0] linno = line[1] - msg = (u':'.join(line[2:])).strip() + msg = (':'.join(line[2:])).strip() print_error(file_, linno, msg) errors += 1 diff --git a/setup.cfg b/setup.cfg index bef998fb37..ab43acfbc5 100644 --- a/setup.cfg +++ b/setup.cfg @@ -18,7 +18,6 @@ Topic :: Home Automation [flake8] max-line-length = 120 -builtins = unicode, long, raw_input, basestring exclude = api_pb2.py [bdist_wheel] diff --git a/setup.py b/setup.py index 1d4fb3a4d0..c520b949fc 100755 --- a/setup.py +++ b/setup.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 """esphome setup script.""" from setuptools import setup, find_packages import os @@ -28,7 +28,6 @@ REQUIRES = [ 'paho-mqtt==1.5.0', 'colorlog==4.0.2', 'tornado==5.1.1', - 'typing>=3.6.6;python_version<"3.6"', 'protobuf==3.11.1', 'tzlocal==2.0.0', 'pytz==2019.3', @@ -69,7 +68,7 @@ setup( zip_safe=False, platforms='any', test_suite='tests', - python_requires='>=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,<4.0', + python_requires='>=3.6,<4.0', install_requires=REQUIRES, keywords=['home', 'automation'], entry_points={