1
0
mirror of https://github.com/esphome/esphome.git synced 2025-09-08 06:12:20 +01:00

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
This commit is contained in:
Otto Winter
2019-12-07 18:28:55 +01:00
committed by GitHub
parent b5714cd70f
commit 056c72d50d
78 changed files with 815 additions and 1097 deletions

View File

@@ -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)