heat/tools/custom_guidelines.py
Hervé Beraud 8c96a4d856 Remove six and python 2.7 full support
Six is in use to help us to keep support for python 2.7.
Since the ussuri cycle we decide to remove the python 2.7
support so we can go ahead and also remove six usage from
the python code.

Review process and help
-----------------------
Removing six introduce a lot of changes and an huge amount of modified files
To simplify reviews we decided to split changes into several patches to avoid
painful reviews and avoid mistakes.

To review this patch you can use the six documentation [1] to obtain help and
understand choices.

Additional informations
-----------------------
Changes related to 'six.b(data)' [2]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

six.b [2] encode the given datas in latin-1 in python3 so I did the same
things in this patch.

Latin-1 is equal to iso-8859-1 [3].

This encoding is the default encoding [4] of certain descriptive HTTP
headers.

I suggest to keep latin-1 for the moment and to move to another encoding
in a follow-up patch if needed to move to most powerful encoding (utf8).

HTML4 support utf8 charset and utf8 is the default charset for HTML5 [5].

Note that this commit message is autogenerated and not necesserly contains
changes related to 'six.b'

[1] https://six.readthedocs.io/
[2] https://six.readthedocs.io/#six.b
[3] https://docs.python.org/3/library/codecs.html#standard-encodings
[4] https://www.w3schools.com/charsets/ref_html_8859.asp
[5] https://www.w3schools.com/html/html_charset.asp

Patch 28 of a serie of 28 patches
six fully removed now!
Thank you six for the rendered services!

Change-Id: If44ee4b565cc9390fa0422fba4dda080b4f90b98
2020-04-23 14:49:12 +02:00

304 lines
13 KiB
Python

#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import argparse
import re
import sys
from oslo_log import log
from heat.common.i18n import _
from heat.engine import constraints
from heat.engine import plugin_manager
from heat.engine import support
LOG = log.getLogger(__name__)
class HeatCustomGuidelines(object):
_RULES = ['resource_descriptions', 'trailing_spaces']
def __init__(self, exclude):
self.error_count = 0
self.resources_classes = []
all_resources = _load_all_resources()
for resource_type in all_resources:
for rsrc_cls in all_resources[resource_type]:
module = rsrc_cls.__module__
# Skip hidden resources check guidelines
if rsrc_cls.support_status.status == support.HIDDEN:
continue
# Skip resources, which defined as template resource in
# environment or cotrib resource
if module in ('heat.engine.resources.template_resource',
'heat.engine.plugins'):
continue
# Skip manually excluded folders
path = module.replace('.', '/')
if any(path.startswith(excl_path) for excl_path in exclude):
continue
self.resources_classes.append(rsrc_cls)
def run_check(self):
print(_('Heat custom guidelines check started.'))
for rule in self._RULES:
getattr(self, 'check_%s' % rule)()
if self.error_count > 0:
print(_('Heat custom guidelines check failed - '
'found %s errors.') % self.error_count)
sys.exit(1)
else:
print(_('Heat custom guidelines check succeeded.'))
def check_resource_descriptions(self):
for cls in self.resources_classes:
# check resource's description
self._check_resource_description(cls)
# check properties' descriptions
self._check_resource_schemas(cls, cls.properties_schema,
'property')
# check attributes' descriptions
self._check_resource_schemas(cls, cls.attributes_schema,
'attribute')
# check methods descriptions
self._check_resource_methods(cls)
def _check_resource_description(self, resource):
description = resource.__doc__
if resource.support_status.status not in (support.SUPPORTED,
support.UNSUPPORTED):
return
kwargs = {'path': resource.__module__, 'details': resource.__name__}
if not description:
kwargs.update({'message': _("Resource description missing, "
"should add resource description "
"about resource's purpose")})
self.print_guideline_error(**kwargs)
return
doclines = [key.strip() for key in description.split('\n')]
if len(doclines) == 1 or (len(doclines) == 2 and doclines[-1] == ''):
kwargs.update({'message': _("Resource description missing, "
"should add resource description "
"about resource's purpose")})
self.print_guideline_error(**kwargs)
return
self._check_description_summary(doclines[0], kwargs, 'resource')
self._check_description_details(doclines, kwargs, 'resource')
def _check_resource_schemas(self, resource, schema, schema_name,
error_path=None):
for key, value in schema.items():
if error_path is None:
error_path = [resource.__name__, key]
else:
error_path.append(key)
# need to check sub-schema of current schema, if exists
if (hasattr(value, 'schema') and
getattr(value, 'schema') is not None):
self._check_resource_schemas(resource, value.schema,
schema_name, error_path)
description = value.description
kwargs = {'path': resource.__module__, 'details': error_path}
if description is None:
if (value.support_status.status == support.SUPPORTED and
not isinstance(value.schema,
constraints.AnyIndexDict) and
not isinstance(schema, constraints.AnyIndexDict)):
kwargs.update({'message': _("%s description "
"missing, need to add "
"description about property's "
"purpose") % schema_name})
self.print_guideline_error(**kwargs)
error_path.pop()
continue
self._check_description_summary(description, kwargs, schema_name)
error_path.pop()
def _check_resource_methods(self, resource):
for method in resource.__dict__.values():
# need to skip non-functions attributes
if not callable(method):
continue
description = method.__doc__
if not description:
continue
if method.__name__.startswith('__'):
continue
doclines = [key.strip() for key in description.split('\n')]
kwargs = {'path': resource.__module__,
'details': [resource.__name__, method.__name__]}
self._check_description_summary(doclines[0], kwargs, 'method')
if len(doclines) == 2:
kwargs.update({'message': _('Method description summary '
'should be in one line')})
self.print_guideline_error(**kwargs)
continue
if len(doclines) > 1:
self._check_description_details(doclines, kwargs, 'method')
def check_trailing_spaces(self):
for cls in self.resources_classes:
try:
cls_file = open(cls.__module__.replace('.', '/') + '.py')
except IOError as ex:
LOG.warning('Cannot perform trailing spaces check on '
'resource module: %s', str(ex))
continue
lines = [line.strip() for line in cls_file.readlines()]
idx = 0
kwargs = {'path': cls.__module__}
while idx < len(lines):
if ('properties_schema' in lines[idx] or
'attributes_schema' in lines[idx]):
level = len(re.findall(r'(\{|\()', lines[idx]))
level -= len(re.findall(r'(\}|\))', lines[idx]))
idx += 1
while level != 0:
level += len(re.findall(r'(\{|\()', lines[idx]))
level -= len(re.findall(r'(\}|\))', lines[idx]))
if re.search("^((\'|\") )", lines[idx]):
kwargs.update(
{'details': 'line %s' % idx,
'message': _('Trailing whitespace should '
'be on previous line'),
'snippet': lines[idx]})
self.print_guideline_error(**kwargs)
elif (re.search("(\\S(\'|\"))$", lines[idx - 1]) and
re.search("^((\'|\")\\S)", lines[idx])):
kwargs.update(
{'details': 'line %s' % (idx - 1),
'message': _('Omitted whitespace at the '
'end of the line'),
'snippet': lines[idx - 1]})
self.print_guideline_error(**kwargs)
idx += 1
idx += 1
def _check_description_summary(self, description, error_kwargs,
error_key):
if re.search("^[a-z]", description):
error_kwargs.update(
{'message': _('%s description summary should start '
'with uppercase letter') % error_key.title(),
'snippet': description})
self.print_guideline_error(**error_kwargs)
if not (description.endswith('.') or description.endswith('.)')):
error_kwargs.update(
{'message': _('%s description summary omitted '
'terminator at the end') % error_key.title(),
'snippet': description})
self.print_guideline_error(**error_kwargs)
if re.search(r"\s{2,}", description):
error_kwargs.update(
{'message': _('%s description contains double or more '
'whitespaces') % error_key.title(),
'snippet': description})
self.print_guideline_error(**error_kwargs)
def _check_description_details(self, doclines, error_kwargs,
error_key):
if re.search(r"\S", doclines[1]):
error_kwargs.update(
{'message': _('%s description summary and '
'main resource description should be '
'separated by blank line') % error_key.title(),
'snippet': doclines[0]})
self.print_guideline_error(**error_kwargs)
if re.search("^[a-z]", doclines[2]):
error_kwargs.update(
{'message': _('%s description should start '
'with with uppercase '
'letter') % error_key.title(),
'snippet': doclines[2]})
self.print_guideline_error(**error_kwargs)
if doclines[-1] != '':
error_kwargs.update(
{'message': _('%s description multistring '
'should have singly closing quotes at '
'the next line') % error_key.title(),
'snippet': doclines[-1]})
self.print_guideline_error(**error_kwargs)
params = False
for line in doclines[1:]:
if re.search(r"\s{2,}", line):
error_kwargs.update(
{'message': _('%s description '
'contains double or more '
'whitespaces') % error_key.title(),
'snippet': line})
self.print_guideline_error(**error_kwargs)
if re.search("^(:param|:type|:returns|:rtype|:raises)",
line):
params = True
if not params and not (doclines[-2].endswith('.') or
doclines[-2].endswith('.)')):
error_kwargs.update(
{'message': _('%s description omitted '
'terminator at the end') % error_key.title(),
'snippet': doclines[-2]})
self.print_guideline_error(**error_kwargs)
def print_guideline_error(self, path, details, message, snippet=None):
if isinstance(details, list):
details = '.'.join(details)
msg = _('ERROR (in %(path)s: %(details)s): %(message)s') % {
'message': message,
'path': path.replace('.', '/'),
'details': details
}
if snippet is not None:
msg = _('%(msg)s\n (Error snippet): %(snippet)s') % {
'msg': msg,
'snippet': '%s...' % snippet[:79]
}
print(msg)
self.error_count += 1
def _load_all_resources():
manager = plugin_manager.PluginManager('heat.engine.resources')
resource_mapping = plugin_manager.PluginMapping('resource')
res_plugin_mappings = resource_mapping.load_all(manager)
all_resources = {}
for mapping in res_plugin_mappings:
name, cls = mapping
if all_resources.get(name) is not None:
all_resources[name].append(cls)
else:
all_resources[name] = [cls]
return all_resources
def parse_args():
parser = argparse.ArgumentParser()
parser.add_argument('--exclude', '-e', metavar='<FOLDER>',
nargs='+',
help=_('Exclude specified paths from checking.'))
return parser.parse_args()
if __name__ == '__main__':
args = parse_args()
guidelines = HeatCustomGuidelines(args.exclude or [])
guidelines.run_check()