summaryrefslogtreecommitdiff
path: root/tools/custom_guidelines.py
diff options
context:
space:
mode:
authorPeter Razumovsky <prazumovsky@mirantis.com>2016-02-18 12:50:29 +0300
committerPeter Razumovsky <prazumovsky@mirantis.com>2016-02-19 18:37:59 +0300
commit874a9ba7da19e3e8e5a391ce07494c1f1dd1105f (patch)
treea09035edddb0f65c4962cc3724e0f6b402470bac /tools/custom_guidelines.py
parent536c8580a01f7b76e79868696cba159ecb3440da (diff)
downloadheat-874a9ba7da19e3e8e5a391ce07494c1f1dd1105f.tar.gz
Implement custom guidelines
Implement custom guidelines for next cases: * resources descriptions * properties schemas descriptions * attributes schemas descriptions * methods descriptions * trailing spaces in attributes and properties descriptions Also, enable custom guidelines check in pep8 checking. implements bp custom-guidelines Change-Id: Ic7b1061abfb42880c476602edb45bc40da291eb0
Diffstat (limited to 'tools/custom_guidelines.py')
-rw-r--r--tools/custom_guidelines.py274
1 files changed, 274 insertions, 0 deletions
diff --git a/tools/custom_guidelines.py b/tools/custom_guidelines.py
new file mode 100644
index 000000000..8f435af6a
--- /dev/null
+++ b/tools/custom_guidelines.py
@@ -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()