Merge "Implement custom guidelines"
This commit is contained in:
commit
1350ee4b7d
274
tools/custom_guidelines.py
Normal file
274
tools/custom_guidelines.py
Normal file
@ -0,0 +1,274 @@
|
||||
#
|
||||
# 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 six
|
||||
import sys
|
||||
|
||||
from heat.common.i18n import _
|
||||
from heat.engine import constraints
|
||||
from heat.engine import resources
|
||||
from heat.engine import support
|
||||
|
||||
|
||||
class HeatCustomGuidelines(object):
|
||||
|
||||
_RULES = []
|
||||
|
||||
def __init__(self, exclude):
|
||||
self.error_count = 0
|
||||
self.resources_classes = []
|
||||
global_env = resources.global_env()
|
||||
for resource_type in global_env.get_types():
|
||||
cls = global_env.get_class(resource_type)
|
||||
if (lambda module: True
|
||||
if [path for path in exclude if path in module]
|
||||
else False)(cls.__module__.replace('.', '/')):
|
||||
continue
|
||||
self.resources_classes.append(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)
|
||||
# TODO(prazumovsky): remove when at least one check will be
|
||||
# available
|
||||
pass
|
||||
|
||||
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 six.iteritems(schema):
|
||||
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 six.itervalues(resource.__dict__):
|
||||
# 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:
|
||||
cls_file = open(cls.__module__.replace('.', '/') + '.py')
|
||||
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('(\{|\()', lines[idx]))
|
||||
level -= len(re.findall('(\}|\))', lines[idx]))
|
||||
idx += 1
|
||||
while level != 0:
|
||||
level += len(re.findall('(\{|\()', lines[idx]))
|
||||
level -= len(re.findall('(\}|\))', 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('.'):
|
||||
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("\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("\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("\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('.'):
|
||||
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 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()
|
1
tox.ini
1
tox.ini
@ -37,6 +37,7 @@ commands =
|
||||
flake8 heat bin/heat-api bin/heat-api-cfn bin/heat-api-cloudwatch bin/heat-engine bin/heat-manage contrib heat_integrationtests doc/source
|
||||
# Check that .po and .pot files are valid:
|
||||
bash -c "find heat -type f -regex '.*\.pot?' -print0|xargs -0 -n 1 msgfmt --check-format -o /dev/null"
|
||||
python tools/custom_guidelines.py --exclude heat/engine/resources/aws
|
||||
|
||||
[testenv:venv]
|
||||
commands = {posargs}
|
||||
|
Loading…
Reference in New Issue
Block a user