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:
@@ -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)
|
||||
|
Reference in New Issue
Block a user