mirror of
https://github.com/esphome/esphome.git
synced 2025-09-06 13:22:19 +01:00
Merge branch 'dev' into bump-1.15.0b1
This commit is contained in:
@@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
||||
# source: api_options.proto
|
||||
|
||||
|
@@ -1,6 +1,19 @@
|
||||
"""Python 3 script to automatically generate C++ classes for ESPHome's native API.
|
||||
|
||||
It's pretty crappy spaghetti code, but it works.
|
||||
|
||||
you need to install protobuf-compiler:
|
||||
running protc --version should return
|
||||
libprotoc 3.6.1
|
||||
|
||||
then run this script with python3 and the files
|
||||
|
||||
esphome/components/api/api_pb2_service.h
|
||||
esphome/components/api/api_pb2_service.cpp
|
||||
esphome/components/api/api_pb2.h
|
||||
esphome/components/api/api_pb2.cpp
|
||||
|
||||
will be generated, they still need to be formatted
|
||||
"""
|
||||
|
||||
import re
|
||||
@@ -10,24 +23,28 @@ from subprocess import call
|
||||
|
||||
# Generate with
|
||||
# protoc --python_out=script/api_protobuf -I esphome/components/api/ api_options.proto
|
||||
|
||||
import api_options_pb2 as pb
|
||||
import google.protobuf.descriptor_pb2 as descriptor
|
||||
|
||||
cwd = Path(__file__).parent
|
||||
file_header = '// This file was automatically generated with a tool.\n'
|
||||
file_header += '// See scripts/api_protobuf/api_protobuf.py\n'
|
||||
|
||||
cwd = Path(__file__).resolve().parent
|
||||
root = cwd.parent.parent / 'esphome' / 'components' / 'api'
|
||||
prot = cwd / 'api.protoc'
|
||||
call(['protoc', '-o', prot, '-I', root, 'api.proto'])
|
||||
prot = root / 'api.protoc'
|
||||
call(['protoc', '-o', str(prot), '-I', str(root), 'api.proto'])
|
||||
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):
|
||||
@@ -344,7 +361,7 @@ class UInt32Type(TypeInfo):
|
||||
class EnumType(TypeInfo):
|
||||
@property
|
||||
def cpp_type(self):
|
||||
return "Enum" + self._field.type_name[1:]
|
||||
return f'enums::{self._field.type_name[1:]}'
|
||||
|
||||
@property
|
||||
def decode_varint(self):
|
||||
@@ -415,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
|
||||
@@ -497,17 +514,17 @@ class RepeatedTypeInfo(TypeInfo):
|
||||
|
||||
|
||||
def build_enum_type(desc):
|
||||
name = "Enum" + desc.name
|
||||
name = desc.name
|
||||
out = f"enum {name} : uint32_t {{\n"
|
||||
for v in desc.value:
|
||||
out += f' {v.name} = {v.number},\n'
|
||||
out += '};\n'
|
||||
|
||||
cpp = f"template<>\n"
|
||||
cpp += f"const char *proto_enum_to_string<{name}>({name} value) {{\n"
|
||||
cpp += f"const char *proto_enum_to_string<enums::{name}>(enums::{name} value) {{\n"
|
||||
cpp += f" switch (value) {{\n"
|
||||
for v in desc.value:
|
||||
cpp += f' case {v.name}: return "{v.name}";\n'
|
||||
cpp += f' case enums::{v.name}: return "{v.name}";\n'
|
||||
cpp += f' default: return "UNKNOWN";\n'
|
||||
cpp += f' }}\n'
|
||||
cpp += f'}}\n'
|
||||
@@ -617,7 +634,8 @@ def build_message_type(desc):
|
||||
|
||||
|
||||
file = d.file[0]
|
||||
content = '''\
|
||||
content = file_header
|
||||
content += '''\
|
||||
#pragma once
|
||||
|
||||
#include "proto.h"
|
||||
@@ -627,7 +645,8 @@ namespace api {
|
||||
|
||||
'''
|
||||
|
||||
cpp = '''\
|
||||
cpp = file_header
|
||||
cpp += '''\
|
||||
#include "api_pb2.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
@@ -636,11 +655,15 @@ namespace api {
|
||||
|
||||
'''
|
||||
|
||||
content += 'namespace enums {\n\n'
|
||||
|
||||
for enum in file.enum_type:
|
||||
s, c = build_enum_type(enum)
|
||||
content += s
|
||||
cpp += c
|
||||
|
||||
content += '\n} // namespace enums\n\n'
|
||||
|
||||
mt = file.message_type
|
||||
|
||||
for m in mt:
|
||||
@@ -708,7 +731,7 @@ def build_service_message_type(mt):
|
||||
cout += f'bool {class_name}::{func}(const {mt.name} &msg) {{\n'
|
||||
if log:
|
||||
cout += f' ESP_LOGVV(TAG, "{func}: %s", msg.dump().c_str());\n'
|
||||
cout += f' this->set_nodelay({str(nodelay).lower()});\n'
|
||||
# cout += f' this->set_nodelay({str(nodelay).lower()});\n'
|
||||
cout += f' return this->send_message_<{mt.name}>(msg, {id_});\n'
|
||||
cout += f'}}\n'
|
||||
if source in (SOURCE_BOTH, SOURCE_CLIENT):
|
||||
@@ -735,7 +758,8 @@ def build_service_message_type(mt):
|
||||
return hout, cout
|
||||
|
||||
|
||||
hpp = '''\
|
||||
hpp = file_header
|
||||
hpp += '''\
|
||||
#pragma once
|
||||
|
||||
#include "api_pb2.h"
|
||||
@@ -746,7 +770,8 @@ namespace api {
|
||||
|
||||
'''
|
||||
|
||||
cpp = '''\
|
||||
cpp = file_header
|
||||
cpp += '''\
|
||||
#include "api_pb2_service.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
|
96
script/build_codeowners.py
Executable file
96
script/build_codeowners.py
Executable file
@@ -0,0 +1,96 @@
|
||||
#!/usr/bin/env python3
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import argparse
|
||||
from collections import defaultdict
|
||||
|
||||
from esphome.helpers import write_file_if_changed
|
||||
from esphome.config import get_component, get_platform
|
||||
from esphome.core import CORE
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--check', help="Check if the CODEOWNERS file is up to date.",
|
||||
action='store_true')
|
||||
args = parser.parse_args()
|
||||
|
||||
# The root directory of the repo
|
||||
root = Path(__file__).parent.parent
|
||||
components_dir = root / 'esphome' / 'components'
|
||||
|
||||
BASE = """
|
||||
# This file is generated by script/build_codeowners.py
|
||||
# People marked here will be automatically requested for a review
|
||||
# when the code that they own is touched.
|
||||
#
|
||||
# Every time an issue is created with a label corresponding to an integration,
|
||||
# the integration's code owner is automatically notified.
|
||||
|
||||
# Core Code
|
||||
setup.py @esphome/core
|
||||
esphome/*.py @esphome/core
|
||||
esphome/core/* @esphome/core
|
||||
|
||||
# Integrations
|
||||
""".strip()
|
||||
|
||||
parts = [BASE]
|
||||
|
||||
# Fake some diretory so that get_component works
|
||||
CORE.config_path = str(root)
|
||||
|
||||
codeowners = defaultdict(list)
|
||||
|
||||
for path in components_dir.iterdir():
|
||||
if not path.is_dir():
|
||||
continue
|
||||
if not (path / '__init__.py').is_file():
|
||||
continue
|
||||
|
||||
name = path.name
|
||||
comp = get_component(name)
|
||||
codeowners[f'esphome/components/{name}/*'].extend(comp.codeowners)
|
||||
|
||||
for platform_path in path.iterdir():
|
||||
platform_name = platform_path.stem
|
||||
platform = get_platform(platform_name, name)
|
||||
if platform is None:
|
||||
continue
|
||||
|
||||
if platform_path.is_dir():
|
||||
# Sub foldered platforms get their own line
|
||||
if not (platform_path / '__init__.py').is_file():
|
||||
continue
|
||||
codeowners[f'esphome/components/{name}/{platform_name}/*'].extend(platform.codeowners)
|
||||
continue
|
||||
|
||||
# Non-subfoldered platforms add to codeowners at component level
|
||||
if not platform_path.is_file() or platform_path.name == '__init__.py':
|
||||
continue
|
||||
codeowners[f'esphome/components/{name}/*'].extend(platform.codeowners)
|
||||
|
||||
|
||||
for path, owners in sorted(codeowners.items()):
|
||||
owners = sorted(set(owners))
|
||||
if not owners:
|
||||
continue
|
||||
for owner in owners:
|
||||
if not owner.startswith('@'):
|
||||
print(f"Codeowner {owner} for integration {path} must start with an '@' symbol!")
|
||||
sys.exit(1)
|
||||
parts.append(f"{path} {' '.join(owners)}")
|
||||
|
||||
|
||||
# End newline
|
||||
parts.append('')
|
||||
content = '\n'.join(parts)
|
||||
codeowners_file = root / 'CODEOWNERS'
|
||||
|
||||
if args.check:
|
||||
if codeowners_file.read_text() != content:
|
||||
print("CODEOWNERS file is not up to date.")
|
||||
print("Please run `script/build_codeowners.py`")
|
||||
sys.exit(1)
|
||||
print("CODEOWNERS file is up to date")
|
||||
else:
|
||||
write_file_if_changed(codeowners_file, content)
|
||||
print("Wrote CODEOWNERS")
|
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python
|
||||
#!/usr/bin/env python3
|
||||
import sys
|
||||
import os.path
|
||||
|
||||
|
85
script/bump-version.py
Executable file
85
script/bump-version.py
Executable file
@@ -0,0 +1,85 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import argparse
|
||||
import re
|
||||
import subprocess
|
||||
from dataclasses import dataclass
|
||||
import sys
|
||||
|
||||
|
||||
@dataclass
|
||||
class Version:
|
||||
major: int
|
||||
minor: int
|
||||
patch: int
|
||||
beta: int = 0
|
||||
dev: bool = False
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.major}.{self.minor}.{self.full_patch}'
|
||||
|
||||
@property
|
||||
def full_patch(self):
|
||||
res = f'{self.patch}'
|
||||
if self.beta > 0:
|
||||
res += f'b{self.beta}'
|
||||
if self.dev:
|
||||
res += '-dev'
|
||||
return res
|
||||
|
||||
@classmethod
|
||||
def parse(cls, value):
|
||||
match = re.match(r'(\d+).(\d+).(\d+)(b\d+)?(-dev)?', value)
|
||||
assert match is not None
|
||||
major = int(match[1])
|
||||
minor = int(match[2])
|
||||
patch = int(match[3])
|
||||
beta = int(match[4][1:]) if match[4] else 0
|
||||
dev = bool(match[5])
|
||||
return Version(
|
||||
major=major, minor=minor, patch=patch,
|
||||
beta=beta, dev=dev
|
||||
)
|
||||
|
||||
|
||||
def sub(path, pattern, repl, expected_count=1):
|
||||
with open(path) as fh:
|
||||
content = fh.read()
|
||||
content, count = re.subn(pattern, repl, content, flags=re.MULTILINE)
|
||||
if expected_count is not None:
|
||||
assert count == expected_count, f"Pattern {pattern} replacement failed!"
|
||||
with open(path, "wt") as fh:
|
||||
fh.write(content)
|
||||
|
||||
|
||||
def write_version(version: Version):
|
||||
sub(
|
||||
'esphome/const.py',
|
||||
r"^MAJOR_VERSION = \d+$",
|
||||
f"MAJOR_VERSION = {version.major}"
|
||||
)
|
||||
sub(
|
||||
'esphome/const.py',
|
||||
r"^MINOR_VERSION = \d+$",
|
||||
f"MINOR_VERSION = {version.minor}"
|
||||
)
|
||||
sub(
|
||||
'esphome/const.py',
|
||||
r"^PATCH_VERSION = .*$",
|
||||
f"PATCH_VERSION = '{version.full_patch}'"
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('new_version', type=str)
|
||||
args = parser.parse_args()
|
||||
|
||||
version = Version.parse(args.new_version)
|
||||
print(f"Bumping to {version}")
|
||||
write_version(version)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main() or 0)
|
@@ -1,10 +1,10 @@
|
||||
#!/usr/bin/env python
|
||||
from __future__ import print_function
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import codecs
|
||||
import collections
|
||||
import fnmatch
|
||||
import os.path
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import re
|
||||
@@ -92,7 +92,8 @@ def lint_post_check(func):
|
||||
|
||||
|
||||
def lint_re_check(regex, **kwargs):
|
||||
prog = re.compile(regex, re.MULTILINE)
|
||||
flags = kwargs.pop('flags', re.MULTILINE)
|
||||
prog = re.compile(regex, flags)
|
||||
decor = lint_content_check(**kwargs)
|
||||
|
||||
def decorator(func):
|
||||
@@ -102,10 +103,12 @@ def lint_re_check(regex, **kwargs):
|
||||
if 'NOLINT' in match.group(0):
|
||||
continue
|
||||
lineno = content.count("\n", 0, match.start()) + 1
|
||||
substr = content[:match.start()]
|
||||
col = len(substr) - substr.rfind('\n')
|
||||
err = func(fname, match)
|
||||
if err is None:
|
||||
continue
|
||||
errors.append("{} See line {}.".format(err, lineno))
|
||||
errors.append((lineno, col+1, err))
|
||||
return errors
|
||||
return decor(new_func)
|
||||
return decorator
|
||||
@@ -122,8 +125,7 @@ def lint_content_find_check(find, **kwargs):
|
||||
errors = []
|
||||
for line, col in find_all(content, find_):
|
||||
err = func(fname)
|
||||
errors.append("{err} See line {line}:{col}."
|
||||
"".format(err=err, line=line+1, col=col+1))
|
||||
errors.append((line+1, col+1, err))
|
||||
return errors
|
||||
return decor(new_func)
|
||||
return decorator
|
||||
@@ -134,7 +136,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 +179,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,
|
||||
@@ -216,9 +218,10 @@ def lint_const_ordered(fname, content):
|
||||
continue
|
||||
target = next(i for i, l in ordered if l == ml)
|
||||
target_text = next(l for i, l in matching if target == i)
|
||||
errors.append("Constant {} is not ordered, please make sure all constants are ordered. "
|
||||
"See line {} (should go to line {}, {})"
|
||||
"".format(highlight(ml), mi, target, target_text))
|
||||
errors.append((ml, None,
|
||||
"Constant {} is not ordered, please make sure all constants are ordered. "
|
||||
"See line {} (should go to line {}, {})"
|
||||
"".format(highlight(ml), mi, target, target_text)))
|
||||
return errors
|
||||
|
||||
|
||||
@@ -253,6 +256,63 @@ def lint_conf_from_const_py(fname, match):
|
||||
"const.py directly.".format(highlight(name)))
|
||||
|
||||
|
||||
RAW_PIN_ACCESS_RE = r'^\s(pinMode|digitalWrite|digitalRead)\((.*)->get_pin\(\),\s*([^)]+).*\)'
|
||||
|
||||
|
||||
@lint_re_check(RAW_PIN_ACCESS_RE, include=cpp_include)
|
||||
def lint_no_raw_pin_access(fname, match):
|
||||
func = match.group(1)
|
||||
pin = match.group(2)
|
||||
mode = match.group(3)
|
||||
new_func = {
|
||||
'pinMode': 'pin_mode',
|
||||
'digitalWrite': 'digital_write',
|
||||
'digitalRead': 'digital_read',
|
||||
}[func]
|
||||
new_code = highlight(f'{pin}->{new_func}({mode})')
|
||||
return (f"Don't use raw {func} calls. Instead, use the `->{new_func}` function: {new_code}")
|
||||
|
||||
|
||||
# Functions from Arduino framework that are forbidden to use directly
|
||||
ARDUINO_FORBIDDEN = [
|
||||
'digitalWrite', 'digitalRead', 'pinMode',
|
||||
'shiftOut', 'shiftIn',
|
||||
'radians', 'degrees',
|
||||
'interrupts', 'noInterrupts',
|
||||
'lowByte', 'highByte',
|
||||
'bitRead', 'bitSet', 'bitClear', 'bitWrite',
|
||||
'bit', 'analogRead', 'analogWrite',
|
||||
'pulseIn', 'pulseInLong',
|
||||
'tone',
|
||||
]
|
||||
ARDUINO_FORBIDDEN_RE = r'[^\w\d](' + r'|'.join(ARDUINO_FORBIDDEN) + r')\(.*'
|
||||
|
||||
|
||||
@lint_re_check(ARDUINO_FORBIDDEN_RE, include=cpp_include, exclude=[
|
||||
'esphome/components/mqtt/custom_mqtt_device.h',
|
||||
'esphome/core/esphal.*',
|
||||
])
|
||||
def lint_no_arduino_framework_functions(fname, match):
|
||||
nolint = highlight("// NOLINT")
|
||||
return (
|
||||
f"The function {highlight(match.group(1))} from the Arduino framework is forbidden to be "
|
||||
f"used directly in the ESPHome codebase. Please use ESPHome's abstractions and equivalent "
|
||||
f"C++ instead.\n"
|
||||
f"\n"
|
||||
f"(If the function is strictly necessary, please add `{nolint}` to the end of the line)"
|
||||
)
|
||||
|
||||
|
||||
@lint_re_check(r'[^\w\d]byte\s+[\w\d]+\s*=.*', include=cpp_include, exclude={
|
||||
'esphome/components/tuya/tuya.h',
|
||||
})
|
||||
def lint_no_byte_datatype(fname, match):
|
||||
return (
|
||||
f"The datatype {highlight('byte')} is not allowed to be used in ESPHome. "
|
||||
f"Please use {highlight('uint8_t')} instead."
|
||||
)
|
||||
|
||||
|
||||
@lint_post_check
|
||||
def lint_constants_usage():
|
||||
errors = []
|
||||
@@ -268,7 +328,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 +344,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 +363,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 ' \
|
||||
@@ -326,6 +386,24 @@ def lint_pragma_once(fname, content):
|
||||
return None
|
||||
|
||||
|
||||
@lint_re_check(r'(whitelist|blacklist|slave)', exclude=['script/ci-custom.py'],
|
||||
flags=re.IGNORECASE | re.MULTILINE)
|
||||
def lint_inclusive_language(fname, match):
|
||||
# From https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=49decddd39e5f6132ccd7d9fdc3d7c470b0061bb
|
||||
return ("Avoid the use of whitelist/blacklist/slave.\n"
|
||||
"Recommended replacements for 'master / slave' are:\n"
|
||||
" '{primary,main} / {secondary,replica,subordinate}\n"
|
||||
" '{initiator,requester} / {target,responder}'\n"
|
||||
" '{controller,host} / {device,worker,proxy}'\n"
|
||||
" 'leader / follower'\n"
|
||||
" 'director / performer'\n"
|
||||
"\n"
|
||||
"Recommended replacements for 'blacklist/whitelist' are:\n"
|
||||
" 'denylist / allowlist'\n"
|
||||
" 'blocklist / passlist'")
|
||||
|
||||
|
||||
|
||||
@lint_content_find_check('ESP_LOG', include=['*.h', '*.tcc'], exclude=[
|
||||
'esphome/components/binary_sensor/binary_sensor.h',
|
||||
'esphome/components/cover/cover.h',
|
||||
@@ -355,13 +433,22 @@ errors = collections.defaultdict(list)
|
||||
def add_errors(fname, errs):
|
||||
if not isinstance(errs, list):
|
||||
errs = [errs]
|
||||
errs = [x for x in errs if x is not None]
|
||||
for err in errs:
|
||||
if not isinstance(err, str):
|
||||
if err is None:
|
||||
continue
|
||||
try:
|
||||
lineno, col, msg = err
|
||||
except ValueError:
|
||||
lineno = 1
|
||||
col = 1
|
||||
msg = err
|
||||
if not isinstance(msg, str):
|
||||
raise ValueError("Error is not instance of string!")
|
||||
if not errs:
|
||||
return
|
||||
errors[fname].extend(errs)
|
||||
if not isinstance(lineno, int):
|
||||
raise ValueError("Line number is not an int!")
|
||||
if not isinstance(col, int):
|
||||
raise ValueError("Column number is not an int!")
|
||||
errors[fname].append((lineno, col, msg))
|
||||
|
||||
|
||||
for fname in files:
|
||||
@@ -380,9 +467,9 @@ 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))
|
||||
for err in errs:
|
||||
print(err)
|
||||
print(f"\033[0;32m************* File \033[1;32m{f}\033[0m")
|
||||
for lineno, col, msg in errs:
|
||||
print(f"ERROR {f}:{lineno}:{col} - {msg}")
|
||||
print()
|
||||
|
||||
sys.exit(len(errors))
|
||||
|
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
|
@@ -1,21 +1,20 @@
|
||||
#!/usr/bin/env python
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import argparse
|
||||
import multiprocessing
|
||||
import os
|
||||
import re
|
||||
|
||||
import pexpect
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
import argparse
|
||||
import click
|
||||
import threading
|
||||
|
||||
import click
|
||||
import pexpect
|
||||
|
||||
sys.path.append(os.path.dirname(__file__))
|
||||
from helpers import basepath, shlex_quote, get_output, build_compile_commands, \
|
||||
build_all_include, temp_header_file, git_ls_files, filter_changed
|
||||
@@ -49,7 +48,7 @@ def run_tidy(args, tmpdir, queue, lock, failed_files):
|
||||
|
||||
# Use pexpect for a pseudy-TTY with colored output
|
||||
output, rc = pexpect.run(invocation_s, withexitstatus=True, encoding='utf-8',
|
||||
timeout=15*60)
|
||||
timeout=15 * 60)
|
||||
with lock:
|
||||
if rc != 0:
|
||||
print()
|
||||
@@ -65,6 +64,11 @@ def progress_bar_show(value):
|
||||
return ''
|
||||
|
||||
|
||||
def split_list(a, n):
|
||||
k, m = divmod(len(a), n)
|
||||
return [a[i * k + min(i, m):(i + 1) * k + min(i + 1, m)] for i in range(n)]
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('-j', '--jobs', type=int,
|
||||
@@ -77,6 +81,10 @@ def main():
|
||||
help='Run clang-tidy in quiet mode')
|
||||
parser.add_argument('-c', '--changed', action='store_true',
|
||||
help='Only run on changed files')
|
||||
parser.add_argument('--split-num', type=int, help='Split the files into X jobs.',
|
||||
default=None)
|
||||
parser.add_argument('--split-at', type=int, help='Which split is this? Starts at 1',
|
||||
default=None)
|
||||
parser.add_argument('--all-headers', action='store_true',
|
||||
help='Create a dummy file that checks all headers')
|
||||
args = parser.parse_args()
|
||||
@@ -114,7 +122,10 @@ def main():
|
||||
|
||||
files.sort()
|
||||
|
||||
if args.all_headers:
|
||||
if args.split_num:
|
||||
files = split_list(files, args.split_num)[args.split_at - 1]
|
||||
|
||||
if args.all_headers and args.split_at in (None, 1):
|
||||
files.insert(0, temp_header_file)
|
||||
|
||||
tmpdir = None
|
||||
@@ -157,8 +168,8 @@ def main():
|
||||
print('Error applying fixes.\n', file=sys.stderr)
|
||||
raise
|
||||
|
||||
sys.exit(return_code)
|
||||
return return_code
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
sys.exit(main())
|
||||
|
@@ -9,4 +9,5 @@ set -x
|
||||
script/ci-custom.py
|
||||
script/lint-python
|
||||
script/lint-cpp
|
||||
script/unit_test
|
||||
script/test
|
||||
|
@@ -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]
|
||||
@@ -101,8 +101,10 @@ def splitlines_no_ends(string):
|
||||
|
||||
|
||||
def changed_files():
|
||||
for remote in ('upstream', 'origin'):
|
||||
command = ['git', 'merge-base', '{}/dev'.format(remote), 'HEAD']
|
||||
check_remotes = ['upstream', 'origin']
|
||||
check_remotes.extend(splitlines_no_ends(get_output('git', 'remote')))
|
||||
for remote in check_remotes:
|
||||
command = ['git', 'merge-base', f'refs/remotes/{remote}/dev', 'HEAD']
|
||||
try:
|
||||
merge_base = splitlines_no_ends(get_output(*command))[0]
|
||||
break
|
||||
@@ -124,7 +126,7 @@ def filter_changed(files):
|
||||
if not files:
|
||||
print(" No changed files!")
|
||||
for c in files:
|
||||
print(" {}".format(c))
|
||||
print(f" {c}")
|
||||
return files
|
||||
|
||||
|
||||
|
@@ -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
|
||||
|
||||
|
@@ -4,5 +4,5 @@
|
||||
set -e
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
pip install -r requirements_test.txt
|
||||
pip install -e .
|
||||
pip3 install -r requirements.txt -r requirements_test.txt
|
||||
pip3 install -e .
|
||||
|
@@ -9,3 +9,4 @@ set -x
|
||||
esphome tests/test1.yaml compile
|
||||
esphome tests/test2.yaml compile
|
||||
esphome tests/test3.yaml compile
|
||||
esphome tests/test4.yaml compile
|
||||
|
9
script/unit_test
Executable file
9
script/unit_test
Executable file
@@ -0,0 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
set -x
|
||||
|
||||
pytest tests/unit_tests
|
Reference in New Issue
Block a user