* Setup pre-commit and black

update pre-commit

add setup

* format with black

format and flake
This commit is contained in:
Guillermo Ruffino 2021-03-07 15:29:02 -03:00 committed by GitHub
parent d8862928c5
commit 0915aba828
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 333 additions and 257 deletions

25
.pre-commit-config.yaml Normal file
View File

@ -0,0 +1,25 @@
# See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks
repos:
- repo: https://github.com/ambv/black
rev: 20.8b1
hooks:
- id: black
args:
- --safe
- --quiet
- repo: https://gitlab.com/pycqa/flake8
rev: 3.8.4
hooks:
- id: flake8
additional_dependencies:
- flake8-docstrings==1.5.0
- pydocstyle==5.1.1
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v3.4.0
hooks:
- id: no-commit-to-branch
args:
- --branch=current
- --branch=next
- --branch=beta

95
conf.py
View File

@ -22,14 +22,10 @@
# sys.path.insert(0, os.path.abspath('.')) # sys.path.insert(0, os.path.abspath('.'))
import hashlib import hashlib
import os import os
import subprocess
from sphinx import addnodes
from sphinx.util.docfields import Field, GroupedField
import re
import sys import sys
sys.path.append(os.path.abspath('.')) sys.path.append(os.path.abspath("."))
# -- General configuration ------------------------------------------------ # -- General configuration ------------------------------------------------
@ -41,58 +37,58 @@ sys.path.append(os.path.abspath('.'))
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones. # ones.
extensions = [ extensions = [
'github', "github",
'seo', "seo",
'sitemap', "sitemap",
] ]
# Add any paths that contain templates here, relative to this directory. # Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates'] templates_path = ["_templates"]
# The suffix(es) of source filenames. # The suffix(es) of source filenames.
# You can specify multiple suffix as a list of string: # You can specify multiple suffix as a list of string:
# #
# source_suffix = ['.rst', '.md'] # source_suffix = ['.rst', '.md']
source_suffix = '.rst' source_suffix = ".rst"
# The master toctree document. # The master toctree document.
master_doc = 'index' master_doc = "index"
# General information about the project. # General information about the project.
project = 'ESPHome' project = "ESPHome"
copyright = '2019, Otto Winter' copyright = "2019, Otto Winter"
html_show_copyright = False html_show_copyright = False
html_show_sphinx = False html_show_sphinx = False
author = 'Otto Winter' author = "Otto Winter"
# The version info for the project you're documenting, acts as replacement for # The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the # |version| and |release|, also used in various other places throughout the
# built documents. # built documents.
# #
# The short X.Y version. # The short X.Y version.
version = '1.17' version = "1.17"
# The full version, including alpha/beta/rc tags. # The full version, including alpha/beta/rc tags.
release = '1.17.0-dev' release = "1.17.0-dev"
# The language for content autogenerated by Sphinx. Refer to documentation # The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages. # for a list of supported languages.
# #
# This is also used if you do content translation via gettext catalogs. # This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases. # Usually you set "language" from the command line for these cases.
language = 'en' language = "en"
# List of patterns, relative to source directory, that match files and # List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files. # directories to ignore when looking for source files.
# This patterns also effect to html_static_path and html_extra_path # This patterns also effect to html_static_path and html_extra_path
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
# The reST default role (used for this markup: `text`) to use for all documents. # The reST default role (used for this markup: `text`) to use for all documents.
# default_role = 'cpp:any' # default_role = 'cpp:any'
# The name of the Pygments (syntax highlighting) style to use. # The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'xcode' pygments_style = "xcode"
highlight_language = 'yaml' highlight_language = "yaml"
primary_domain = None primary_domain = None
@ -105,40 +101,40 @@ todo_include_todos = False
# The theme to use for HTML and HTML Help pages. See the documentation for # The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes. # a list of builtin themes.
# #
html_theme = 'alabaster' html_theme = "alabaster"
# Theme options are theme-specific and customize the look and feel of a theme # Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the # further. For a list of options available for each theme, see the
# documentation. # documentation.
# #
html_baseurl = os.getenv('BASE_URL', 'https://esphome.io') html_baseurl = os.getenv("BASE_URL", "https://esphome.io")
with open('_static/custom.css', 'rb') as f: with open("_static/custom.css", "rb") as f:
custom_css_hash = hashlib.md5(f.read()).hexdigest()[:8] custom_css_hash = hashlib.md5(f.read()).hexdigest()[:8]
html_theme_options = { html_theme_options = {
# 'logo': 'logo-full.png', # 'logo': 'logo-full.png',
'logo_name': False, "logo_name": False,
'show_related': False, "show_related": False,
'sidebar_collapse': True, "sidebar_collapse": True,
'fixed_sidebar': True, "fixed_sidebar": True,
'show_powered_by': False, "show_powered_by": False,
} }
html_context = { html_context = {
'custom_css_hash': custom_css_hash, "custom_css_hash": custom_css_hash,
} }
html_logo = 'images/logo-text.svg' html_logo = "images/logo-text.svg"
html_copy_source = True html_copy_source = True
html_show_sourcelink = False html_show_sourcelink = False
html_last_updated_fmt = None html_last_updated_fmt = None
html_use_smartypants = False html_use_smartypants = False
html_title = 'ESPHome' html_title = "ESPHome"
# Add any paths that contain custom static files (such as style sheets) here, # Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files, # relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css". # so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static'] html_static_path = ["_static"]
# Custom sidebar templates, must be a dictionary that maps document names # Custom sidebar templates, must be a dictionary that maps document names
# to template names. # to template names.
@ -146,10 +142,10 @@ html_static_path = ['_static']
# This is required for the alabaster theme # This is required for the alabaster theme
# refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars # refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars
html_sidebars = { html_sidebars = {
'**': [ "**": [
# 'about.html', # 'about.html',
'searchbox.html', "searchbox.html",
'localtoc.html', "localtoc.html",
] ]
} }
@ -157,7 +153,7 @@ html_sidebars = {
# -- Options for HTMLHelp output ------------------------------------------ # -- Options for HTMLHelp output ------------------------------------------
# Output file base name for HTML help builder. # Output file base name for HTML help builder.
htmlhelp_basename = 'esphomedoc' htmlhelp_basename = "esphomedoc"
# -- Options for LaTeX output --------------------------------------------- # -- Options for LaTeX output ---------------------------------------------
@ -166,15 +162,12 @@ latex_elements = {
# The paper size ('letterpaper' or 'a4paper'). # The paper size ('letterpaper' or 'a4paper').
# #
# 'papersize': 'letterpaper', # 'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt'). # The font size ('10pt', '11pt' or '12pt').
# #
# 'pointsize': '10pt', # 'pointsize': '10pt',
# Additional stuff for the LaTeX preamble. # Additional stuff for the LaTeX preamble.
# #
# 'preamble': '', # 'preamble': '',
# Latex figure (float) alignment # Latex figure (float) alignment
# #
# 'figure_align': 'htbp', # 'figure_align': 'htbp',
@ -184,21 +177,17 @@ latex_elements = {
# (source start file, target name, title, # (source start file, target name, title,
# author, documentclass [howto, manual, or own class]). # author, documentclass [howto, manual, or own class]).
latex_documents = [ latex_documents = [
(master_doc, 'esphome.tex', 'ESPHome Documentation', (master_doc, "esphome.tex", "ESPHome Documentation", "Otto Winter", "manual"),
'Otto Winter', 'manual'),
] ]
latex_engine = 'xelatex' latex_engine = "xelatex"
# -- Options for manual page output --------------------------------------- # -- Options for manual page output ---------------------------------------
# One entry per manual page. List of tuples # One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section). # (source start file, name, description, authors, manual section).
man_pages = [ man_pages = [(master_doc, "esphome", "ESPHome Documentation", [author], 1)]
(master_doc, 'esphome', 'ESPHome Documentation',
[author], 1)
]
# -- Options for Texinfo output ------------------------------------------- # -- Options for Texinfo output -------------------------------------------
@ -207,8 +196,14 @@ man_pages = [
# (source start file, target name, title, author, # (source start file, target name, title, author,
# dir menu entry, description, category) # dir menu entry, description, category)
texinfo_documents = [ texinfo_documents = [
(master_doc, 'esphome', 'ESPHome Documentation', (
author, 'esphome', 'One line description of project.', master_doc,
'Miscellaneous'), "esphome",
"ESPHome Documentation",
author,
"esphome",
"One line description of project.",
"Miscellaneous",
),
] ]
linkcheck_ignore = [r'https://github.com/.*', r'https://discord.gg/.*'] linkcheck_ignore = [r"https://github.com/.*", r"https://discord.gg/.*"]

165
github.py
View File

@ -9,39 +9,35 @@ from docutils.parsers.rst import directives
from docutils.parsers.rst.directives.tables import Table from docutils.parsers.rst.directives.tables import Table
def libpr_role(name, rawtext, text, lineno, inliner, options=None, def libpr_role(name, rawtext, text, lineno, inliner, options=None, content=None):
content=None): ref = "https://github.com/esphome/esphome-core/pull/{}".format(text)
ref = 'https://github.com/esphome/esphome-core/pull/{}'.format(text) return [make_link_node(rawtext, "core#{}".format(text), ref, options)], []
return [make_link_node(rawtext, 'core#{}'.format(text), ref, options)], []
def yamlpr_role(name, rawtext, text, lineno, inliner, options=None, def yamlpr_role(name, rawtext, text, lineno, inliner, options=None, content=None):
content=None): ref = "https://github.com/esphome/esphome/pull/{}".format(text)
ref = 'https://github.com/esphome/esphome/pull/{}'.format(text) return [make_link_node(rawtext, "esphome#{}".format(text), ref, options)], []
return [make_link_node(rawtext, 'esphome#{}'.format(text), ref, options)], []
def docspr_role(name, rawtext, text, lineno, inliner, options=None, def docspr_role(name, rawtext, text, lineno, inliner, options=None, content=None):
content=None): ref = "https://github.com/esphome/esphome-docs/pull/{}".format(text)
ref = 'https://github.com/esphome/esphome-docs/pull/{}'.format(text) return [make_link_node(rawtext, "docs#{}".format(text), ref, options)], []
return [make_link_node(rawtext, 'docs#{}'.format(text), ref, options)], []
def ghuser_role(name, rawtext, text, lineno, inliner, options=None, def ghuser_role(name, rawtext, text, lineno, inliner, options=None, content=None):
content=None): ref = "https://github.com/{}".format(text)
ref = 'https://github.com/{}'.format(text) return [make_link_node(rawtext, "@{}".format(text), ref, options)], []
return [make_link_node(rawtext, '@{}'.format(text), ref, options)], []
value_re = re.compile(r'^(.*)\s*<(.*)>$') value_re = re.compile(r"^(.*)\s*<(.*)>$")
DOXYGEN_LOOKUP = {} DOXYGEN_LOOKUP = {}
for s in string.ascii_lowercase + string.digits: for s in string.ascii_lowercase + string.digits:
DOXYGEN_LOOKUP[s] = s DOXYGEN_LOOKUP[s] = s
for s in string.ascii_uppercase: for s in string.ascii_uppercase:
DOXYGEN_LOOKUP[s] = '_{}'.format(s.lower()) DOXYGEN_LOOKUP[s] = "_{}".format(s.lower())
DOXYGEN_LOOKUP[':'] = '_1' DOXYGEN_LOOKUP[":"] = "_1"
DOXYGEN_LOOKUP['_'] = '__' DOXYGEN_LOOKUP["_"] = "__"
DOXYGEN_LOOKUP['.'] = '_8' DOXYGEN_LOOKUP["."] = "_8"
def split_text_value(value): def split_text_value(value):
@ -52,60 +48,57 @@ def split_text_value(value):
def encode_doxygen(value): def encode_doxygen(value):
value = value.split('/')[-1] value = value.split("/")[-1]
try: try:
return ''.join(DOXYGEN_LOOKUP[s] for s in value) return "".join(DOXYGEN_LOOKUP[s] for s in value)
except KeyError: except KeyError:
raise ValueError("Unknown character in doxygen string! '{}'".format(value)) raise ValueError("Unknown character in doxygen string! '{}'".format(value))
def apiref_role(name, rawtext, text, lineno, inliner, options=None, def apiref_role(name, rawtext, text, lineno, inliner, options=None, content=None):
content=None):
text, value = split_text_value(text) text, value = split_text_value(text)
if text is None: if text is None:
text = 'API Reference' text = "API Reference"
ref = '/api/{}.html'.format(encode_doxygen(value)) ref = "/api/{}.html".format(encode_doxygen(value))
return [make_link_node(rawtext, text, ref, options)], [] return [make_link_node(rawtext, text, ref, options)], []
def apiclass_role(name, rawtext, text, lineno, inliner, options=None, def apiclass_role(name, rawtext, text, lineno, inliner, options=None, content=None):
content=None):
text, value = split_text_value(text) text, value = split_text_value(text)
if text is None: if text is None:
text = value text = value
ref = '/api/classesphome_1_1{}.html'.format(encode_doxygen(value)) ref = "/api/classesphome_1_1{}.html".format(encode_doxygen(value))
return [make_link_node(rawtext, text, ref, options)], [] return [make_link_node(rawtext, text, ref, options)], []
def apistruct_role(name, rawtext, text, lineno, inliner, options=None, def apistruct_role(name, rawtext, text, lineno, inliner, options=None, content=None):
content=None):
text, value = split_text_value(text) text, value = split_text_value(text)
if text is None: if text is None:
text = value text = value
ref = '/api/structesphome_1_1{}.html'.format(encode_doxygen(value)) ref = "/api/structesphome_1_1{}.html".format(encode_doxygen(value))
return [make_link_node(rawtext, text, ref, options)], [] return [make_link_node(rawtext, text, ref, options)], []
def ghedit_role(name, rawtext, text, lineno, inliner, options=None, def ghedit_role(name, rawtext, text, lineno, inliner, options=None, content=None):
content=None): path = os.path.relpath(
path = os.path.relpath(inliner.document.current_source, inliner.document.current_source, inliner.document.settings.env.app.srcdir
inliner.document.settings.env.app.srcdir) )
ref = 'https://github.com/esphome/esphome-docs/blob/current/{}'.format(path) ref = "https://github.com/esphome/esphome-docs/blob/current/{}".format(path)
return [make_link_node(rawtext, 'Edit this page on GitHub', ref, options)], [] return [make_link_node(rawtext, "Edit this page on GitHub", ref, options)], []
def make_link_node(rawtext, text, ref, options=None): def make_link_node(rawtext, text, ref, options=None):
options = options or {} options = options or {}
node = nodes.reference(rawtext, node = nodes.reference(rawtext, utils.unescape(text), refuri=ref, **options)
utils.unescape(text),
refuri=ref,
**options)
return node return node
# https://stackoverflow.com/a/3415150/8924614 # https://stackoverflow.com/a/3415150/8924614
def grouper(n, iterable, fillvalue=None): def grouper(n, iterable, fillvalue=None):
"""grouper(3, 'ABCDEFG', 'x') --> ABC DEF Gxx""" """Pythonic way to iterate over sequence, 4 items at a time.
grouper(3, 'ABCDEFG', 'x') --> ABC DEF Gxx
"""
args = [iter(iterable)] * n args = [iter(iterable)] * n
return zip_longest(fillvalue=fillvalue, *args) return zip_longest(fillvalue=fillvalue, *args)
@ -114,12 +107,11 @@ def grouper(n, iterable, fillvalue=None):
class ImageTableDirective(Table): class ImageTableDirective(Table):
option_spec = { option_spec = {
'columns': directives.positive_int, "columns": directives.positive_int,
} }
def run(self): def run(self):
env = self.state.document.settings.env cols = self.options.get("columns", 3)
cols = self.options.get('columns', 3)
items = [] items = []
@ -129,31 +121,30 @@ class ImageTableDirective(Table):
continue continue
name, page, image = row name, page, image = row
link = page.strip() link = page.strip()
if link.startswith('http'): if link.startswith("http"):
pass pass
else: else:
if not link.startswith('/'): if not link.startswith("/"):
link = '/{}'.format(link) link = "/{}".format(link)
if '.html' not in link: if ".html" not in link:
link += '.html' link += ".html"
items.append({ items.append(
'name': name.strip(), {
'link': link, "name": name.strip(),
'image': '/images/{}'.format(image.strip()), "link": link,
}) "image": "/images/{}".format(image.strip()),
}
)
col_widths = self.get_column_widths(cols) col_widths = self.get_column_widths(cols)
title, messages = self.make_title() title, messages = self.make_title()
table = nodes.table() table = nodes.table()
table['classes'].append('table-center') table["classes"].append("table-center")
# Set up column specifications based on widths # Set up column specifications based on widths
tgroup = nodes.tgroup(cols=cols) tgroup = nodes.tgroup(cols=cols)
table += tgroup table += tgroup
tgroup.extend( tgroup.extend(nodes.colspec(colwidth=col_width) for col_width in col_widths)
nodes.colspec(colwidth=col_width)
for col_width in col_widths
)
tbody = nodes.tbody() tbody = nodes.tbody()
tgroup += tbody tgroup += tbody
@ -166,12 +157,12 @@ class ImageTableDirective(Table):
entry += nodes.paragraph() entry += nodes.paragraph()
trow += entry trow += entry
continue continue
name = cell['name'] name = cell["name"]
link = cell['link'] link = cell["link"]
image = cell['image'] image = cell["image"]
reference_node = nodes.reference(refuri=link) reference_node = nodes.reference(refuri=link)
img = nodes.image(uri=directives.uri(image), alt=name) img = nodes.image(uri=directives.uri(image), alt=name)
img['classes'].append('component-image') img["classes"].append("component-image")
reference_node += img reference_node += img
para = nodes.paragraph() para = nodes.paragraph()
para += reference_node para += reference_node
@ -186,8 +177,8 @@ class ImageTableDirective(Table):
entry += nodes.paragraph() entry += nodes.paragraph()
trow += entry trow += entry
continue continue
name = cell['name'] name = cell["name"]
link = cell['link'] link = cell["link"]
ref = nodes.reference(name, name, refuri=link) ref = nodes.reference(name, name, refuri=link)
para = nodes.paragraph() para = nodes.paragraph()
para += ref para += ref
@ -207,8 +198,6 @@ class PinTableDirective(Table):
option_spec = {} option_spec = {}
def run(self): def run(self):
env = self.state.document.settings.env
items = [] items = []
data = list(csv.reader(self.content)) data = list(csv.reader(self.content))
@ -227,18 +216,14 @@ class PinTableDirective(Table):
# Set up column specifications based on widths # Set up column specifications based on widths
tgroup = nodes.tgroup(cols=2) tgroup = nodes.tgroup(cols=2)
table += tgroup table += tgroup
tgroup.extend( tgroup.extend(nodes.colspec(colwidth=col_width) for col_width in col_widths)
nodes.colspec(colwidth=col_width)
for col_width in col_widths
)
thead = nodes.thead() thead = nodes.thead()
tgroup += thead tgroup += thead
trow = nodes.row() trow = nodes.row()
thead += trow thead += trow
trow.extend( trow.extend(
nodes.entry(h, nodes.paragraph(text=h)) nodes.entry(h, nodes.paragraph(text=h)) for h in ("Pin", "Function")
for h in ('Pin', 'Function')
) )
tbody = nodes.tbody() tbody = nodes.tbody()
@ -269,18 +254,16 @@ class PinTableDirective(Table):
def setup(app): def setup(app):
app.add_role('libpr', libpr_role) app.add_role("libpr", libpr_role)
app.add_role('corepr', libpr_role) app.add_role("corepr", libpr_role)
app.add_role('yamlpr', yamlpr_role) app.add_role("yamlpr", yamlpr_role)
app.add_role('esphomepr', yamlpr_role) app.add_role("esphomepr", yamlpr_role)
app.add_role('docspr', docspr_role) app.add_role("docspr", docspr_role)
app.add_role('ghuser', ghuser_role) app.add_role("ghuser", ghuser_role)
app.add_role('apiref', apiref_role) app.add_role("apiref", apiref_role)
app.add_role('apiclass', apiclass_role) app.add_role("apiclass", apiclass_role)
app.add_role('apistruct', apistruct_role) app.add_role("apistruct", apistruct_role)
app.add_role('ghedit', ghedit_role) app.add_role("ghedit", ghedit_role)
app.add_directive('imgtable', ImageTableDirective) app.add_directive("imgtable", ImageTableDirective)
app.add_directive('pintable', PinTableDirective) app.add_directive("pintable", PinTableDirective)
return {"version": "1.0.0", return {"version": "1.0.0", "parallel_read_safe": True, "parallel_write_safe": True}
"parallel_read_safe": True,
"parallel_write_safe": True}

View File

@ -2,7 +2,6 @@
import argparse import argparse
import re import re
import subprocess
from dataclasses import dataclass from dataclasses import dataclass
import sys import sys
@ -16,30 +15,27 @@ class Version:
dev: bool = False dev: bool = False
def __str__(self): def __str__(self):
return f'{self.major}.{self.minor}.{self.full_patch}' return f"{self.major}.{self.minor}.{self.full_patch}"
@property @property
def full_patch(self): def full_patch(self):
res = f'{self.patch}' res = f"{self.patch}"
if self.beta > 0: if self.beta > 0:
res += f'b{self.beta}' res += f"b{self.beta}"
if self.dev: if self.dev:
res += '-dev' res += "-dev"
return res return res
@classmethod @classmethod
def parse(cls, value): def parse(cls, value):
match = re.match(r'(\d+).(\d+).(\d+)(b\d+)?(-dev)?', value) match = re.match(r"(\d+).(\d+).(\d+)(b\d+)?(-dev)?", value)
assert match is not None assert match is not None
major = int(match[1]) major = int(match[1])
minor = int(match[2]) minor = int(match[2])
patch = int(match[3]) patch = int(match[3])
beta = int(match[4][1:]) if match[4] else 0 beta = int(match[4][1:]) if match[4] else 0
dev = bool(match[5]) dev = bool(match[5])
return Version( return Version(major=major, minor=minor, patch=patch, beta=beta, dev=dev)
major=major, minor=minor, patch=patch,
beta=beta, dev=dev
)
def sub(path, pattern, repl, expected_count=1): def sub(path, pattern, repl, expected_count=1):
@ -55,35 +51,25 @@ def sub(path, pattern, repl, expected_count=1):
def write_version(version: Version): def write_version(version: Version):
# ESPHOME_REF = v1.14.4 # ESPHOME_REF = v1.14.4
sub( sub(
'Makefile', "Makefile",
r'ESPHOME_REF = .*', r"ESPHOME_REF = .*",
f'ESPHOME_REF = v{version}' if not version.dev else f'ESPHOME_REF = dev' f"ESPHOME_REF = v{version}" if not version.dev else "ESPHOME_REF = dev",
) )
# PROJECT_NUMBER = 1.14.4 # PROJECT_NUMBER = 1.14.4
sub( sub(
'Doxygen', "Doxygen", r"PROJECT_NUMBER = .*", f"PROJECT_NUMBER = {version}"
r'PROJECT_NUMBER = .*',
f'PROJECT_NUMBER = {version}'
) )
# version = '1.14' # version = '1.14'
sub( sub("conf.py", r"version = '.*'", f"version = '{version.major}.{version.minor}'")
'conf.py',
r"version = '.*'",
f"version = '{version.major}.{version.minor}'"
)
# release = '1.14.4' # release = '1.14.4'
sub( sub("conf.py", r"release = '.*'", f"release = '{version}'")
'conf.py', with open("_static/version", "wt") as fh:
r"release = '.*'",
f"release = '{version}'"
)
with open('_static/version', 'wt') as fh:
fh.write(str(version)) fh.write(str(version))
def main(): def main():
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument('new_version', type=str) parser.add_argument("new_version", type=str)
args = parser.parse_args() args = parser.parse_args()
version = Version.parse(args.new_version) version = Version.parse(args.new_version)

80
seo.py
View File

@ -6,11 +6,18 @@ from docutils.writers._html_base import HTMLTranslator
class SEONode(nodes.General, nodes.Element): class SEONode(nodes.General, nodes.Element):
def __init__(self, title=None, description=None, image=None, def __init__(
author=None, author_twitter=None, keywords=None): self,
title=None,
description=None,
image=None,
author=None,
author_twitter=None,
keywords=None,
):
super(SEONode, self).__init__() super(SEONode, self).__init__()
self.title = title self.title = title
self.description = description.replace('\n', ' ') self.description = description.replace("\n", " ")
self.image = image self.image = image
self.author = author self.author = author
self.author_twitter = author_twitter self.author_twitter = author_twitter
@ -25,26 +32,34 @@ class RedirectNode(nodes.General, nodes.Element):
def seo_visit(self: HTMLTranslator, node: SEONode): def seo_visit(self: HTMLTranslator, node: SEONode):
def encode_text(text): def encode_text(text):
special_characters = {ord('&'): '&amp;', special_characters = {
ord('<'): '&lt;', ord("&"): "&amp;",
ord('"'): '&quot;', ord("<"): "&lt;",
ord('>'): '&gt;'} ord('"'): "&quot;",
ord(">"): "&gt;",
}
return text.translate(special_characters) return text.translate(special_characters)
def create_content_meta(name, content): def create_content_meta(name, content):
if content is None: if content is None:
return return
self.meta.append('<meta name="{}" content="{}">\n'.format(name, encode_text(content))) self.meta.append(
'<meta name="{}" content="{}">\n'.format(name, encode_text(content))
)
def create_itemprop_meta(name, content): def create_itemprop_meta(name, content):
if content is None: if content is None:
return return
self.meta.append('<meta itemprop="{}" content="{}">\n'.format(name, encode_text(content))) self.meta.append(
'<meta itemprop="{}" content="{}">\n'.format(name, encode_text(content))
)
def create_property_meta(name, content): def create_property_meta(name, content):
if content is None: if content is None:
return return
self.meta.append('<meta property="{}" content="{}">\n'.format(name, encode_text(content))) self.meta.append(
'<meta property="{}" content="{}">\n'.format(name, encode_text(content))
)
# Base # Base
create_content_meta("description", node.description) create_content_meta("description", node.description)
@ -76,8 +91,11 @@ def seo_visit(self: HTMLTranslator, node: SEONode):
def redirect_visit(self: HTMLTranslator, node: RedirectNode): def redirect_visit(self: HTMLTranslator, node: RedirectNode):
self.meta.append('<meta http-equiv="refresh" content="0; url={}">'.format(node.url)) self.meta.append('<meta http-equiv="refresh" content="0; url={}">'.format(node.url))
self.body.append(self.starttag(node, 'p', self.body.append(
'Redirecting to <a href="{0}">{0}</a>'.format(node.url))) self.starttag(
node, "p", 'Redirecting to <a href="{0}">{0}</a>'.format(node.url)
)
)
def seo_depart(self, _): def seo_depart(self, _):
@ -85,36 +103,36 @@ def seo_depart(self, _):
def redirect_depart(self, _): def redirect_depart(self, _):
self.body.append('</p>') self.body.append("</p>")
class SEODirective(Directive): class SEODirective(Directive):
option_spec = { option_spec = {
'title': directives.unchanged, "title": directives.unchanged,
'description': directives.unchanged, "description": directives.unchanged,
'image': directives.path, "image": directives.path,
'author': directives.unchanged, "author": directives.unchanged,
'author_twitter': directives.unchanged, "author_twitter": directives.unchanged,
'keywords': directives.unchanged, "keywords": directives.unchanged,
} }
def run(self): def run(self):
env = self.state.document.settings.env env = self.state.document.settings.env
title_match = re.match(r'.+<title>(.+)</title>.+', str(self.state.document)) title_match = re.match(r".+<title>(.+)</title>.+", str(self.state.document))
if title_match is not None and 'title' not in self.options: if title_match is not None and "title" not in self.options:
self.options['title'] = title_match.group(1) self.options["title"] = title_match.group(1)
image = self.options.get('image') image = self.options.get("image")
if image is not None: if image is not None:
if not image.startswith('/'): if not image.startswith("/"):
image = '/_images/' + image image = "/_images/" + image
self.options['image'] = env.config.html_baseurl + image self.options["image"] = env.config.html_baseurl + image
return [SEONode(**self.options)] return [SEONode(**self.options)]
class RedirectDirective(Directive): class RedirectDirective(Directive):
option_spec = { option_spec = {
'url': directives.unchanged, "url": directives.unchanged,
} }
def run(self): def run(self):
@ -122,10 +140,8 @@ class RedirectDirective(Directive):
def setup(app): def setup(app):
app.add_directive('seo', SEODirective) app.add_directive("seo", SEODirective)
app.add_node(SEONode, html=(seo_visit, seo_depart)) app.add_node(SEONode, html=(seo_visit, seo_depart))
app.add_directive('redirect', RedirectDirective) app.add_directive("redirect", RedirectDirective)
app.add_node(RedirectNode, html=(redirect_visit, redirect_depart)) app.add_node(RedirectNode, html=(redirect_visit, redirect_depart))
return {"version": "1.0.0", return {"version": "1.0.0", "parallel_read_safe": True, "parallel_write_safe": True}
"parallel_read_safe": True,
"parallel_write_safe": True}

41
setup.cfg Normal file
View File

@ -0,0 +1,41 @@
[flake8]
max-line-length = 120
# Following 4 for black compatibility
# E501: line too long
# W503: Line break occurred before a binary operator
# E203: Whitespace before ':'
# D202 No blank lines allowed after function docstring
# TODO fix flake8
# D100 Missing docstring in public module
# D101 Missing docstring in public class
# D102 Missing docstring in public method
# D103 Missing docstring in public function
# D104 Missing docstring in public package
# D105 Missing docstring in magic method
# D107 Missing docstring in __init__
# D200 One-line docstring should fit on one line with quotes
# D205 1 blank line required between summary line and description
# D209 Multi-line docstring closing quotes should be on a separate line
# D400 First line should end with a period
# D401 First line should be in imperative mood
ignore =
E501,
W503,
E203,
D202,
D100,
D101,
D102,
D103,
D104,
D105,
D107,
D200,
D205,
D209,
D400,
D401,

View File

@ -4,15 +4,17 @@ import xml.etree.ElementTree as ET
def setup(app): def setup(app):
"""Setup connects events to the sitemap builder""" """Setup connects events to the sitemap builder"""
app.connect('html-page-context', add_html_link) app.connect("html-page-context", add_html_link)
app.connect('build-finished', create_sitemap) app.connect("build-finished", create_sitemap)
app.sitemap_links = [] app.sitemap_links = []
is_production = os.getenv('PRODUCTION') == 'YES' is_production = os.getenv("PRODUCTION") == "YES"
return {"version": "1.0.0", return {
"parallel_read_safe": True, "version": "1.0.0",
"parallel_write_safe": not is_production} "parallel_read_safe": True,
"parallel_write_safe": not is_production,
}
def add_html_link(app, pagename, templatename, context, doctree): def add_html_link(app, pagename, templatename, context, doctree):
@ -29,26 +31,27 @@ def create_sitemap(app, exception):
for link in app.sitemap_links: for link in app.sitemap_links:
url = ET.SubElement(root, "url") url = ET.SubElement(root, "url")
priority = 0.5 priority = 0.5
if link == 'index.html': if link == "index.html":
priority = 1.0 priority = 1.0
link = '' link = ""
elif link.endswith('index.html'): elif link.endswith("index.html"):
priority += 0.25 priority += 0.25
link = link[:-len('index.html')] link = link[: -len("index.html")]
if link.endswith('.html'): if link.endswith(".html"):
link = link[:-len('.html')] link = link[: -len(".html")]
ET.SubElement(url, "loc").text = app.builder.config.html_baseurl + '/' + link ET.SubElement(url, "loc").text = app.builder.config.html_baseurl + "/" + link
ET.SubElement(url, "priority").text = str(priority) ET.SubElement(url, "priority").text = str(priority)
filename = os.path.join(app.outdir, "sitemap.xml") filename = os.path.join(app.outdir, "sitemap.xml")
ET.ElementTree(root).write(filename, ET.ElementTree(root).write(
xml_declaration=True, filename, xml_declaration=True, encoding="utf-8", method="xml"
encoding='utf-8', )
method="xml")
with open(os.path.join(app.builder.outdir, 'robots.txt'), 'wt') as f: with open(os.path.join(app.builder.outdir, "robots.txt"), "wt") as f:
if os.getenv('PRODUCTION') != 'YES': if os.getenv("PRODUCTION") != "YES":
f.write('User-agent: *\nDisallow: /\n') f.write("User-agent: *\nDisallow: /\n")
else: else:
f.write('User-agent: *\nDisallow: \n\n' f.write(
'Sitemap: https://esphome.io/sitemap.xml\n') "User-agent: *\nDisallow: \n\n"
"Sitemap: https://esphome.io/sitemap.xml\n"
)

View File

@ -7,13 +7,16 @@ import queue
import sys import sys
to_p = Path('svg2png') to_p = Path("svg2png")
to_p.mkdir(exist_ok=True) to_p.mkdir(exist_ok=True)
for f in to_p.glob('*.png'): for f in to_p.glob("*.png"):
f.unlink() f.unlink()
images = [f for f in Path('_build/html/_images/').glob('*.svg') images = [
if not re.match(r'^seg[0-9A-F]{2}$', f.stem)] f
for f in Path("_build/html/_images/").glob("*.svg")
if not re.match(r"^seg[0-9A-F]{2}$", f.stem)
]
q = queue.Queue() q = queue.Queue()
@ -23,18 +26,28 @@ def worker():
if item is None: if item is None:
break break
to = to_p / item.with_suffix('.png').name to = to_p / item.with_suffix(".png").name
args = ['inkscape', '-z', '-e', str(to.absolute()), '-w', '800', args = [
'-background', 'white', str(item.absolute())] "inkscape",
print("Running: {}".format(' '.join(shlex.quote(x) for x in args))) "-z",
"-e",
str(to.absolute()),
"-w",
"800",
"-background",
"white",
str(item.absolute()),
]
print("Running: {}".format(" ".join(shlex.quote(x) for x in args)))
proc = subprocess.run(args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) proc = subprocess.run(args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
if b'Bitmap saved as' not in proc.stdout: if b"Bitmap saved as" not in proc.stdout:
print("Error!") print("Error!")
print(proc.stdout) print(proc.stdout)
sys.exit(1) sys.exit(1)
q.task_done() q.task_done()
NUM_THREADS = 8 NUM_THREADS = 8
threads = [] threads = []
for i in range(NUM_THREADS): for i in range(NUM_THREADS):

View File

@ -16,32 +16,40 @@ def find_all(a_str, sub):
column += len(sub) column += len(sub)
section_regex = re.compile(r'^(=+|-+|\*+|~+)$') section_regex = re.compile(r"^(=+|-+|\*+|~+)$")
directive_regex = re.compile(r'^(\s*)\.\. (.*)::.*$') directive_regex = re.compile(r"^(\s*)\.\. (.*)::.*$")
directive_arg_regex = re.compile(r'^(\s+):.*:\s*.*$') directive_arg_regex = re.compile(r"^(\s+):.*:\s*.*$")
esphome_io_regex = re.compile(r'https://esphome.io/') esphome_io_regex = re.compile(r"https://esphome.io/")
for f in sorted(Path('.').glob('**/*.rst')): for f in sorted(Path(".").glob("**/*.rst")):
try: try:
content = f.read_text('utf-8') content = f.read_text("utf-8")
except UnicodeDecodeError: except UnicodeDecodeError:
errors.append("File {} is not readable as UTF-8. Please set your editor to UTF-8 mode." errors.append(
"".format(f)) "File {} is not readable as UTF-8. Please set your editor to UTF-8 mode."
"".format(f)
)
continue continue
if not content.endswith('\n'): if not content.endswith("\n"):
errors.append("Newline at end of file missing. Please insert an empty line at end " errors.append(
"of file {}".format(f)) "Newline at end of file missing. Please insert an empty line at end "
"of file {}".format(f)
)
# Check tab character # Check tab character
for line, col in find_all(content, '\t'): for line, col in find_all(content, "\t"):
errors.append("File {} contains tab character on line {}:{}. " errors.append(
"Please convert tabs to spaces.".format(f, line + 1, col)) "File {} contains tab character on line {}:{}. "
"Please convert tabs to spaces.".format(f, line + 1, col)
)
# Check windows newline # Check windows newline
for line, col in find_all(content, '\r'): for line, col in find_all(content, "\r"):
errors.append("File {} contains windows newline on line {}:{}. " errors.append(
"Please set your editor to unix newline mode.".format(f, line + 1, col)) "File {} contains windows newline on line {}:{}. "
"Please set your editor to unix newline mode.".format(f, line + 1, col)
)
lines = content.splitlines(keepends=False) lines = content.splitlines(keepends=False)
@ -65,7 +73,7 @@ for f in sorted(Path('.').glob('**/*.rst')):
continue continue
base_indentation = len(m.group(1)) base_indentation = len(m.group(1))
directive_name = m.group(2) directive_name = m.group(2)
if directive_name.startswith('|') or directive_name == 'seo': if directive_name.startswith("|") or directive_name == "seo":
continue continue
# Match directive args # Match directive args
for j in range(i + 1, len(lines)): for j in range(i + 1, len(lines)):
@ -77,8 +85,10 @@ for f in sorted(Path('.').glob('**/*.rst')):
# Empty line must follow # Empty line must follow
if lines[j]: if lines[j]:
errors.append("Directive '{}' is not followed by an empty line. Please insert an " errors.append(
"empty line after {}:{}".format(directive_name, f, j)) "Directive '{}' is not followed by an empty line. Please insert an "
"empty line after {}:{}".format(directive_name, f, j)
)
continue continue
k = j + 1 k = j + 1
@ -93,16 +103,20 @@ for f in sorted(Path('.').glob('**/*.rst')):
break break
num_indent = num_spaces - base_indentation num_indent = num_spaces - base_indentation
if j == k and num_indent != 4: if j == k and num_indent != 4:
errors.append("Directive '{}' must be indented with 4 spaces, not {}. See " errors.append(
"{}:{}".format(directive_name, num_indent, f, j+1)) "Directive '{}' must be indented with 4 spaces, not {}. See "
"{}:{}".format(directive_name, num_indent, f, j + 1)
)
break break
for i, line in enumerate(lines): for i, line in enumerate(lines):
if esphome_io_regex.search(line): if esphome_io_regex.search(line):
if 'privacy.rst' in str(f) or 'web_server.rst' in str(f): if "privacy.rst" in str(f) or "web_server.rst" in str(f):
continue continue
errors.append("All links to esphome.io should be relative, please remove esphome.io " errors.append(
"from URL. See {}:{}".format(f, i+1)) "All links to esphome.io should be relative, please remove esphome.io "
"from URL. See {}:{}".format(f, i + 1)
)
for error in errors: for error in errors: