Add tests.

This commit is contained in:
Monty Taylor 2013-03-19 14:51:17 -07:00
parent b7700e3360
commit 5e64a3a377
4 changed files with 246 additions and 193 deletions

View File

@ -16,13 +16,12 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
"""nova HACKING file compliance testing """OpenStack HACKING file compliance testing
Built on top of pep8.py Built on top of pep8.py
""" """
import imp import imp
import inspect
import logging import logging
import os import os
import re import re
@ -36,15 +35,21 @@ import pep8
# Don't need this for testing # Don't need this for testing
logging.disable('LOG') logging.disable('LOG')
#N1xx comments
#N2xx except def flake8ext(f):
#N3xx imports f.name = __name__
#N4xx docstrings f.version = '0.0.1'
#N5xx dictionaries/lists return f
#N6xx calling methods
#N7xx localization #H1xx comments
#N8xx git commit messages #H2xx except
#N9xx other #H3xx imports
#H4xx docstrings
#H5xx dictionaries/lists
#H6xx calling methods
#H7xx localization
#H8xx git commit messages
#H9xx other
IMPORT_EXCEPTIONS = ['sqlalchemy', 'migrate', 'nova.db.sqlalchemy.session', IMPORT_EXCEPTIONS = ['sqlalchemy', 'migrate', 'nova.db.sqlalchemy.session',
'nova.db.sqlalchemy.migration.versioning_api'] 'nova.db.sqlalchemy.migration.versioning_api']
@ -56,40 +61,6 @@ VERBOSE_MISSING_IMPORT = os.getenv('HACKING_VERBOSE_MISSING_IMPORT', 'False')
_missingImport = set([]) _missingImport = set([])
# Monkey patch broken excluded filter in pep8
# See https://github.com/jcrocholl/pep8/pull/111
def excluded(self, filename):
"""Check if options.exclude contains a pattern that matches filename."""
basename = os.path.basename(filename)
return any((pep8.filename_match(filename, self.options.exclude,
default=False),
pep8.filename_match(basename, self.options.exclude,
default=False)))
def input_dir(self, dirname):
"""Check all files in this directory and all subdirectories."""
dirname = dirname.rstrip('/')
if self.excluded(dirname):
return 0
counters = self.options.report.counters
verbose = self.options.verbose
filepatterns = self.options.filename
runner = self.runner
for root, dirs, files in os.walk(dirname):
if verbose:
print('directory ' + root)
counters['directories'] += 1
for subdir in sorted(dirs):
if self.excluded(os.path.join(root, subdir)):
dirs.remove(subdir)
for filename in sorted(files):
# contain a pattern that matches?
if ((pep8.filename_match(filename, filepatterns) and
not self.excluded(filename))):
runner(os.path.join(root, filename))
def is_import_exception(mod): def is_import_exception(mod):
return (mod in IMPORT_EXCEPTIONS or return (mod in IMPORT_EXCEPTIONS or
any(mod.startswith(m + '.') for m in IMPORT_EXCEPTIONS)) any(mod.startswith(m + '.') for m in IMPORT_EXCEPTIONS))
@ -109,48 +80,51 @@ def import_normalize(line):
return line return line
def nova_todo_format(physical_line, tokens): @flake8ext
def hacking_todo_format(physical_line, tokens):
"""Check for 'TODO()'. """Check for 'TODO()'.
nova HACKING guide recommendation for TODO: nova HACKING guide recommendation for TODO:
Include your name with TODOs as in "#TODO(termie)" Include your name with TODOs as in "#TODO(termie)"
Okay: #TODO(sdague) Okay: #TODO(sdague)
N101: #TODO fail H101: #TODO fail
N101: #TODO (jogo) fail H101: #TODO (jogo) fail
""" """
# TODO(sdague): TODO check shouldn't fail inside of space # TODO(sdague): TODO check shouldn't fail inside of space
pos = physical_line.find('TODO') pos = physical_line.find('TODO')
pos1 = physical_line.find('TODO(') pos1 = physical_line.find('TODO(')
pos2 = physical_line.find('#') # make sure it's a comment pos2 = physical_line.find('#') # make sure it's a comment
if (pos != pos1 and pos2 >= 0 and pos2 < pos and len(tokens) == 0): if (pos != pos1 and pos2 >= 0 and pos2 < pos and len(tokens) == 0):
return pos, "N101: Use TODO(NAME)" return pos, "H101: Use TODO(NAME)"
def nova_except_format(logical_line): @flake8ext
def hacking_except_format(logical_line):
r"""Check for 'except:'. r"""Check for 'except:'.
nova HACKING guide recommends not using except: nova HACKING guide recommends not using except:
Do not write "except:", use "except Exception:" at the very least Do not write "except:", use "except Exception:" at the very least
Okay: except Exception: Okay: try:\n pass\nexcept Exception:\n pass
N201: except: H201: except:
""" """
if logical_line.startswith("except:"): if logical_line.startswith("except:"):
yield 6, "N201: no 'except:' at least use 'except Exception:'" yield 6, "H201: no 'except:' at least use 'except Exception:'"
def nova_except_format_assert(logical_line): @flake8ext
def hacking_except_format_assert(logical_line):
r"""Check for 'assertRaises(Exception'. r"""Check for 'assertRaises(Exception'.
nova HACKING guide recommends not using assertRaises(Exception...): nova HACKING guide recommends not using assertRaises(Exception...):
Do not use overly broad Exception type Do not use overly broad Exception type
Okay: self.assertRaises(NovaException) Okay: self.assertRaises(NovaException)
N202: self.assertRaises(Exception) H202: self.assertRaises(Exception)
""" """
if logical_line.startswith("self.assertRaises(Exception"): if logical_line.startswith("self.assertRaises(Exception"):
yield 1, "N202: assertRaises Exception too broad" yield 1, "H202: assertRaises Exception too broad"
modules_cache = dict((mod, True) for mod in tuple(sys.modules.keys()) modules_cache = dict((mod, True) for mod in tuple(sys.modules.keys())
@ -159,7 +133,8 @@ modules_cache = dict((mod, True) for mod in tuple(sys.modules.keys())
RE_RELATIVE_IMPORT = re.compile('^from\s*[.]') RE_RELATIVE_IMPORT = re.compile('^from\s*[.]')
def nova_import_rules(logical_line): @flake8ext
def hacking_import_rules(logical_line, filename):
r"""Check for imports. r"""Check for imports.
nova HACKING guide recommends one import per line: nova HACKING guide recommends one import per line:
@ -167,7 +142,7 @@ def nova_import_rules(logical_line):
Examples: Examples:
Okay: from nova.compute import api Okay: from nova.compute import api
N301: from nova.compute import api, utils H301: from nova.compute import api, utils
Imports should usually be on separate lines. Imports should usually be on separate lines.
@ -181,13 +156,15 @@ def nova_import_rules(logical_line):
Okay: from os import (path as p) Okay: from os import (path as p)
Okay: import os.path Okay: import os.path
Okay: from nova.compute import rpcapi Okay: from nova.compute import rpcapi
N302: from os.path import dirname as dirname2 H302: from os.path import dirname as dirname2
N302: from os.path import (dirname as dirname2) H302: from os.path import (dirname as dirname2)
N303: from os.path import * H303: from os.path import *
N304: from .compute import rpcapi H304: from .compute import rpcapi
""" """
#NOTE(afazekas): An old style relative import example will not be able to #NOTE(afazekas): An old style relative import example will not be able to
# pass the doctest, since the relativity depends on the file's locality # pass the doctest, since the relativity depends on the file's locality
#TODO(mordred: We need to split this into 4 different checks so that they
# can be disabled by command line switches properly
def is_module_for_sure(mod, search_path=sys.path): def is_module_for_sure(mod, search_path=sys.path):
mod = mod.replace('(', '') # Ignore parentheses mod = mod.replace('(', '') # Ignore parentheses
@ -201,7 +178,7 @@ def nova_import_rules(logical_line):
except ImportError: except ImportError:
try: try:
# NOTE(vish): handle namespace modules # NOTE(vish): handle namespace modules
module = __import__(mod) __import__(mod)
except ImportError, exc: except ImportError, exc:
# NOTE(vish): the import error might be due # NOTE(vish): the import error might be due
# to a missing dependency # to a missing dependency
@ -211,7 +188,7 @@ def nova_import_rules(logical_line):
_missingImport.add(missing) _missingImport.add(missing)
return True return True
return False return False
except Exception, exc: except Exception:
# NOTE(jogo) don't stack trace if unexpected import error, # NOTE(jogo) don't stack trace if unexpected import error,
# log and continue. # log and continue.
traceback.print_exc() traceback.print_exc()
@ -226,8 +203,8 @@ def nova_import_rules(logical_line):
modules_cache[mod] = res modules_cache[mod] = res
return res return res
current_path = os.path.dirname(pep8.current_file) current_path = os.path.dirname(filename)
current_mod = os.path.basename(pep8.current_file) current_mod = os.path.basename(filename)
if current_mod[-3:] == ".py": if current_mod[-3:] == ".py":
current_mod = current_mod[:-3] current_mod = current_mod[:-3]
@ -238,11 +215,11 @@ def nova_import_rules(logical_line):
pos = logical_line.find(',') pos = logical_line.find(',')
if pos != -1: if pos != -1:
if split_line[0] == 'from': if split_line[0] == 'from':
yield pos, "N301: one import per line" yield pos, "H301: one import per line"
return # ',' is not supported by the N302 checker yet return # ',' is not supported by the H302 checker yet
pos = logical_line.find('*') pos = logical_line.find('*')
if pos != -1: if pos != -1:
yield pos, "N303: No wildcard (*) import." yield pos, "H303: No wildcard (*) import."
return return
if split_line_len in (2, 4, 6) and split_line[1] != "__future__": if split_line_len in (2, 4, 6) and split_line[1] != "__future__":
@ -251,13 +228,13 @@ def nova_import_rules(logical_line):
if is_import_exception(mod): if is_import_exception(mod):
return return
if RE_RELATIVE_IMPORT.search(logical_line): if RE_RELATIVE_IMPORT.search(logical_line):
yield logical_line.find('.'), ("N304: No " yield logical_line.find('.'), (
"relative imports. '%s' is a relative import" "H304: No relative imports. '%s' is a relative import"
% logical_line) % logical_line)
return return
if not is_module(mod): if not is_module(mod):
yield 0, ("N302: import only modules." yield 0, ("H302: import only modules."
"'%s' does not import a module" % logical_line) "'%s' does not import a module" % logical_line)
return return
@ -265,26 +242,25 @@ def nova_import_rules(logical_line):
# The import keyword just imports modules # The import keyword just imports modules
# The guestfs module now imports guestfs # The guestfs module now imports guestfs
mod = split_line[1] mod = split_line[1]
if (current_mod != mod and if (current_mod != mod and not is_module(mod) and
not is_module(mod) and
is_module_for_sure(mod, [current_path])): is_module_for_sure(mod, [current_path])):
yield 0, ("N304: No relative imports." yield 0, ("H304: No relative imports."
" '%s' is a relative import" " '%s' is a relative import" % logical_line)
% logical_line)
#TODO(jogo): import template: N305 #TODO(jogo): import template: H305
def nova_import_alphabetical(logical_line, blank_lines, previous_logical, @flake8ext
def hacking_import_alphabetical(logical_line, blank_lines, previous_logical,
indent_level, previous_indent_level): indent_level, previous_indent_level):
r"""Check for imports in alphabetical order. r"""Check for imports in alphabetical order.
nova HACKING guide recommendation for imports: OpenStack HACKING guide recommendation for imports:
imports in human alphabetical order imports in human alphabetical order
Okay: import os\nimport sys\n\nimport nova\nfrom nova import test Okay: import os\nimport sys\n\nimport nova\nfrom nova import test
N306: import sys\nimport os H306: import sys\nimport os
""" """
# handle import x # handle import x
# use .lower since capitalization shouldn't dictate order # use .lower since capitalization shouldn't dictate order
@ -296,21 +272,22 @@ def nova_import_alphabetical(logical_line, blank_lines, previous_logical,
if (len(split_line) in length and len(split_previous) in length and if (len(split_line) in length and len(split_previous) in length and
split_line[0] == "import" and split_previous[0] == "import"): split_line[0] == "import" and split_previous[0] == "import"):
if split_line[1] < split_previous[1]: if split_line[1] < split_previous[1]:
yield (0, "N306: imports not in alphabetical order (%s, %s)" yield (0, "H306: imports not in alphabetical order (%s, %s)"
% (split_previous[1], split_line[1])) % (split_previous[1], split_line[1]))
def nova_import_no_db_in_virt(logical_line, filename): @flake8ext
def hacking_import_no_db_in_virt(logical_line, filename):
"""Check for db calls from nova/virt """Check for db calls from nova/virt
As of grizzly-2 all the database calls have been removed from As of grizzly-2 all the database calls have been removed from
nova/virt, and we want to keep it that way. nova/virt, and we want to keep it that way.
N307 H307
""" """
if "nova/virt" in filename and not filename.endswith("fake.py"): if "nova/virt" in filename and not filename.endswith("fake.py"):
if logical_line.startswith("from nova import db"): if logical_line.startswith("from nova import db"):
yield (0, "N307: nova.db import not allowed in nova/virt/*") yield (0, "H307: nova.db import not allowed in nova/virt/*")
def is_docstring(physical_line, previous_logical): def is_docstring(physical_line, previous_logical):
@ -331,7 +308,8 @@ def is_docstring(physical_line, previous_logical):
return end and start in (-1, len(line) - 4) return end and start in (-1, len(line) - 4)
def nova_docstring_start_space(physical_line, previous_logical): @flake8ext
def hacking_docstring_start_space(physical_line, previous_logical):
r"""Check for docstring not start with space. r"""Check for docstring not start with space.
nova HACKING guide recommendation for docstring: nova HACKING guide recommendation for docstring:
@ -340,11 +318,11 @@ def nova_docstring_start_space(physical_line, previous_logical):
Okay: def foo():\n '''This is good.''' Okay: def foo():\n '''This is good.'''
Okay: def foo():\n a = ''' This is not a docstring.''' Okay: def foo():\n a = ''' This is not a docstring.'''
Okay: def foo():\n pass\n ''' This is not.''' Okay: def foo():\n pass\n ''' This is not.'''
N401: def foo():\n ''' This is not.''' H401: def foo():\n ''' This is not.'''
""" """
# short circuit so that we don't fail on our own fail test # short circuit so that we don't fail on our own fail test
# when running under external pep8 # when running under external pep8
if physical_line.find("N401: def foo()") != -1: if physical_line.find("H401: def foo()") != -1:
return return
# it's important that we determine this is actually a docstring, # it's important that we determine this is actually a docstring,
@ -353,11 +331,12 @@ def nova_docstring_start_space(physical_line, previous_logical):
if is_docstring(physical_line, previous_logical): if is_docstring(physical_line, previous_logical):
pos = max([physical_line.find(i) for i in START_DOCSTRING_TRIPLE]) pos = max([physical_line.find(i) for i in START_DOCSTRING_TRIPLE])
if physical_line[pos + 3] == ' ': if physical_line[pos + 3] == ' ':
return (pos, "N401: docstring should not start with" return (pos, "H401: docstring should not start with"
" a space") " a space")
def nova_docstring_one_line(physical_line, previous_logical): @flake8ext
def hacking_docstring_one_line(physical_line, previous_logical):
r"""Check one line docstring end. r"""Check one line docstring end.
nova HACKING guide recommendation for one line docstring: nova HACKING guide recommendation for one line docstring:
@ -369,9 +348,9 @@ def nova_docstring_one_line(physical_line, previous_logical):
Okay: def foo():\n a = '''This is not a docstring''' Okay: def foo():\n a = '''This is not a docstring'''
Okay: def foo():\n pass\n '''This is not a docstring''' Okay: def foo():\n pass\n '''This is not a docstring'''
Okay: class Foo:\n pass\n '''This is not a docstring''' Okay: class Foo:\n pass\n '''This is not a docstring'''
N402: def foo():\n '''This is not''' H402: def foo():\n '''This is not'''
N402: def foo():\n '''Bad punctuation,''' H402: def foo():\n '''Bad punctuation,'''
N402: class Foo:\n '''Bad punctuation,''' H402: class Foo:\n '''Bad punctuation,'''
""" """
#TODO(jogo) make this apply to multi line docstrings as well #TODO(jogo) make this apply to multi line docstrings as well
line = physical_line.lstrip() line = physical_line.lstrip()
@ -381,10 +360,11 @@ def nova_docstring_one_line(physical_line, previous_logical):
if pos != -1 and end and len(line) > pos + 4: if pos != -1 and end and len(line) > pos + 4:
if line[-5] not in ['.', '?', '!']: if line[-5] not in ['.', '?', '!']:
return pos, "N402: one line docstring needs punctuation." return pos, "H402: one line docstring needs punctuation."
def nova_docstring_multiline_end(physical_line, previous_logical, tokens): @flake8ext
def hacking_docstring_multiline_end(physical_line, previous_logical, tokens):
r"""Check multi line docstring end. r"""Check multi line docstring end.
nova HACKING guide recommendation for docstring: nova HACKING guide recommendation for docstring:
@ -395,8 +375,8 @@ def nova_docstring_multiline_end(physical_line, previous_logical, tokens):
Okay: class Foo:\n '''foobar\nfoo\nbar\n''' Okay: class Foo:\n '''foobar\nfoo\nbar\n'''
Okay: def foo():\n a = '''not\na\ndocstring''' Okay: def foo():\n a = '''not\na\ndocstring'''
Okay: def foo():\n pass\n'''foobar\nfoo\nbar\n d''' Okay: def foo():\n pass\n'''foobar\nfoo\nbar\n d'''
N403: def foo():\n '''foobar\nfoo\nbar\ndocstring''' H403: def foo():\n '''foobar\nfoo\nbar\ndocstring'''
N403: class Foo:\n '''foobar\nfoo\nbar\ndocstring'''\n\n H403: class Foo:\n '''foobar\nfoo\nbar\ndocstring'''\n\n
""" """
# if find OP tokens, not a docstring # if find OP tokens, not a docstring
ops = [t for t, _, _, _, _ in tokens if t == tokenize.OP] ops = [t for t, _, _, _, _ in tokens if t == tokenize.OP]
@ -404,10 +384,11 @@ def nova_docstring_multiline_end(physical_line, previous_logical, tokens):
len(ops) == 0): len(ops) == 0):
pos = max(physical_line.find(i) for i in END_DOCSTRING_TRIPLE) pos = max(physical_line.find(i) for i in END_DOCSTRING_TRIPLE)
if physical_line.strip() not in START_DOCSTRING_TRIPLE: if physical_line.strip() not in START_DOCSTRING_TRIPLE:
return (pos, "N403: multi line docstring end on new line") return (pos, "H403: multi line docstring end on new line")
def nova_docstring_multiline_start(physical_line, previous_logical, tokens): @flake8ext
def hacking_docstring_multiline_start(physical_line, previous_logical, tokens):
r"""Check multi line docstring start with summary. r"""Check multi line docstring start with summary.
nova HACKING guide recommendation for docstring: nova HACKING guide recommendation for docstring:
@ -415,28 +396,29 @@ def nova_docstring_multiline_start(physical_line, previous_logical, tokens):
Okay: '''foobar\nfoo\nbar\n''' Okay: '''foobar\nfoo\nbar\n'''
Okay: def foo():\n a = '''\nnot\na docstring\n''' Okay: def foo():\n a = '''\nnot\na docstring\n'''
N404: def foo():\n'''\nfoo\nbar\n'''\n\n H404: def foo():\n'''\nfoo\nbar\n'''\n\n
""" """
if is_docstring(physical_line, previous_logical): if is_docstring(physical_line, previous_logical):
pos = max([physical_line.find(i) for i in START_DOCSTRING_TRIPLE]) pos = max([physical_line.find(i) for i in START_DOCSTRING_TRIPLE])
# start of docstring when len(tokens)==0 # start of docstring when len(tokens)==0
if len(tokens) == 0 and pos != -1 and len(physical_line) == pos + 4: if len(tokens) == 0 and pos != -1 and len(physical_line) == pos + 4:
if physical_line.strip() in START_DOCSTRING_TRIPLE: if physical_line.strip() in START_DOCSTRING_TRIPLE:
return (pos, "N404: multi line docstring " return (pos, "H404: multi line docstring "
"should start with a summary") "should start with a summary")
def nova_no_cr(physical_line): @flake8ext
def hacking_no_cr(physical_line):
r"""Check that we only use newlines not carriage returns. r"""Check that we only use newlines not carriage returns.
Okay: import os\nimport sys Okay: import os\nimport sys
# pep8 doesn't yet replace \r in strings, will work on an # pep8 doesn't yet replace \r in strings, will work on an
# upstream fix # upstream fix
N901 import os\r\nimport sys H601 import os\r\nimport sys
""" """
pos = physical_line.find('\r') pos = physical_line.find('\r')
if pos != -1 and pos == (len(physical_line) - 2): if pos != -1 and pos == (len(physical_line) - 2):
return (pos, "N901: Windows style line endings not allowed in code") return (pos, "H601: Windows style line endings not allowed in code")
FORMAT_RE = re.compile("%(?:" FORMAT_RE = re.compile("%(?:"
@ -486,43 +468,45 @@ def check_i18n():
break break
if not format_string: if not format_string:
raise LocalizationError(start, raise LocalizationError(
"N701: Empty localization string") start, "H701: Empty localization string")
if token_type != tokenize.OP: if token_type != tokenize.OP:
raise LocalizationError(start, raise LocalizationError(
"N701: Invalid localization call") start, "H701: Invalid localization call")
if text != ")": if text != ")":
if text == "%": if text == "%":
raise LocalizationError(start, raise LocalizationError(
"N702: Formatting operation should be outside" start,
"H702: Formatting operation should be outside"
" of localization method call") " of localization method call")
elif text == "+": elif text == "+":
raise LocalizationError(start, raise LocalizationError(
"N702: Use bare string concatenation instead" start,
" of +") "H702: Use bare string concatenation instead of +")
else: else:
raise LocalizationError(start, raise LocalizationError(
"N702: Argument to _ must be just a string") start, "H702: Argument to _ must be just a string")
format_specs = FORMAT_RE.findall(format_string) format_specs = FORMAT_RE.findall(format_string)
positional_specs = [(key, spec) for key, spec in format_specs positional_specs = [(key, spec) for key, spec in format_specs
if not key and spec] if not key and spec]
# not spec means %%, key means %(smth)s # not spec means %%, key means %(smth)s
if len(positional_specs) > 1: if len(positional_specs) > 1:
raise LocalizationError(start, raise LocalizationError(
"N703: Multiple positional placeholders") start, "H703: Multiple positional placeholders")
def nova_localization_strings(logical_line, tokens): @flake8ext
def hacking_localization_strings(logical_line, tokens):
r"""Check localization in line. r"""Check localization in line.
Okay: _("This is fine") Okay: _("This is fine")
Okay: _("This is also fine %s") Okay: _("This is also fine %s")
N701: _('') H701: _('')
N702: _("Bob" + " foo") H702: _("Bob" + " foo")
N702: _("Bob %s" % foo) H702: _("Bob %s" % foo)
# N703 check is not quite right, disabled by removing colon # H703 check is not quite right, disabled by removing colon
N703 _("%s %s" % (foo, bar)) H703 _("%s %s" % (foo, bar))
""" """
# TODO(sdague) actually get these tests working # TODO(sdague) actually get these tests working
gen = check_i18n() gen = check_i18n()
@ -536,67 +520,46 @@ def nova_localization_strings(logical_line, tokens):
#TODO(jogo) Dict and list objects #TODO(jogo) Dict and list objects
def nova_is_not(logical_line): @flake8ext
def hacking_is_not(logical_line):
r"""Check localization in line. r"""Check localization in line.
Okay: if x is not y Okay: if x is not y:\n pass
N901: if not X is Y H901: if not X is Y
N901: if not X.B is Y H901: if not X.B is Y
""" """
split_line = logical_line.split() split_line = logical_line.split()
if (len(split_line) == 5 and split_line[0] == 'if' and if (len(split_line) == 5 and split_line[0] == 'if' and
split_line[1] == 'not' and split_line[3] == 'is'): split_line[1] == 'not' and split_line[3] == 'is'):
yield (logical_line.find('not'), "N901: Use the 'is not' " yield (logical_line.find('not'), "H901: Use the 'is not' "
"operator for when testing for unequal identities") "operator for when testing for unequal identities")
def nova_not_in(logical_line): @flake8ext
def hacking_not_in(logical_line):
r"""Check localization in line. r"""Check localization in line.
Okay: if x not in y Okay: if x not in y:\n pass
Okay: if not (X in Y or X is Z) Okay: if not (X in Y or X is Z):\n pass
Okay: if not (X in Y) Okay: if not (X in Y):\n pass
N902: if not X in Y H902: if not X in Y
N902: if not X.B in Y H902: if not X.B in Y
""" """
split_line = logical_line.split() split_line = logical_line.split()
if (len(split_line) == 5 and split_line[0] == 'if' and if (len(split_line) == 5 and split_line[0] == 'if' and
split_line[1] == 'not' and split_line[3] == 'in' and not split_line[1] == 'not' and split_line[3] == 'in' and not
split_line[2].startswith('(')): split_line[2].startswith('(')):
yield (logical_line.find('not'), "N902: Use the 'not in' " yield (logical_line.find('not'), "H902: Use the 'not in' "
"operator for collection membership evaluation") "operator for collection membership evaluation")
current_file = ""
def readlines(filename):
"""Record the current file being tested."""
pep8.current_file = filename
return open(filename).readlines()
def add_nova():
"""Monkey patch in nova guidelines.
Look for functions that start with nova_ and have arguments
and add them to pep8 module
Assumes you know how to write pep8.py checks
"""
for name, function in globals().items():
if not inspect.isfunction(function):
continue
args = inspect.getargspec(function)[0]
if args and name.startswith("nova"):
exec("pep8.%s = %s" % (name, name))
def once_git_check_commit_title(): def once_git_check_commit_title():
"""Check git commit messages. """Check git commit messages.
nova HACKING recommends not referencing a bug or blueprint in first line, nova HACKING recommends not referencing a bug or blueprint in first line,
it should provide an accurate description of the change it should provide an accurate description of the change
N801 H801
N802 Title limited to 72 chars H802 Title limited to 72 chars
""" """
#Get title of most recent commit #Get title of most recent commit
@ -617,25 +580,25 @@ def once_git_check_commit_title():
error = False error = False
#NOTE(jogo) if match regex but over 3 words, acceptable title #NOTE(jogo) if match regex but over 3 words, acceptable title
if GIT_REGEX.search(title) is not None and len(title.split()) <= 3: if GIT_REGEX.search(title) is not None and len(title.split()) <= 3:
print ("N801: git commit title ('%s') should provide an accurate " print ("H801: git commit title ('%s') should provide an accurate "
"description of the change, not just a reference to a bug " "description of the change, not just a reference to a bug "
"or blueprint" % title.strip()) "or blueprint" % title.strip())
error = True error = True
# HACKING.rst recommends commit titles 50 chars or less, but enforces # HACKING.rst recommends commit titles 50 chars or less, but enforces
# a 72 character limit # a 72 character limit
if len(title.decode('utf-8')) > 72: if len(title.decode('utf-8')) > 72:
print ("N802: git commit title ('%s') should be under 50 chars" print ("H802: git commit title ('%s') should be under 50 chars"
% title.strip()) % title.strip())
error = True error = True
return error return error
imports_on_separate_lines_N301_compliant = r""" imports_on_separate_lines_H301_compliant = r"""
Imports should usually be on separate lines. Imports should usually be on separate lines.
Okay: import os\nimport sys Okay: import os\nimport sys
E401: import sys, os E401: import sys, os
N301: from subprocess import Popen, PIPE H301: from subprocess import Popen, PIPE
Okay: from myclas import MyClass Okay: from myclas import MyClass
Okay: from foo.bar.yourclass import YourClass Okay: from foo.bar.yourclass import YourClass
Okay: import myclass Okay: import myclass
@ -650,19 +613,15 @@ if __name__ == "__main__":
#NOVA error codes start with an N #NOVA error codes start with an N
pep8.SELFTEST_REGEX = re.compile(r'(Okay|[EWN]\d{3}):\s(.*)') pep8.SELFTEST_REGEX = re.compile(r'(Okay|[EWN]\d{3}):\s(.*)')
pep8.ERRORCODE_REGEX = re.compile(r'[EWN]\d{3}') pep8.ERRORCODE_REGEX = re.compile(r'[EWN]\d{3}')
add_nova()
pep8.current_file = current_file
pep8.readlines = readlines
pep8.StyleGuide.excluded = excluded
pep8.StyleGuide.input_dir = input_dir
# we need to kill this doctring otherwise the self tests fail # we need to kill this doctring otherwise the self tests fail
pep8.imports_on_separate_lines.__doc__ = \ pep8.imports_on_separate_lines.__doc__ = \
imports_on_separate_lines_N301_compliant imports_on_separate_lines_H301_compliant
try: try:
pep8._main() pep8._main()
sys.exit(once_error) sys.exit(once_error)
finally: finally:
if len(_missingImport) > 0: if len(_missingImport) > 0:
print >> sys.stderr, ("%i imports missing in this test environment" print >> sys.stderr, (
"%i imports missing in this test environment"
% len(_missingImport)) % len(_missingImport))

16
hacking/tests/__init__.py Normal file
View File

@ -0,0 +1,16 @@
#!/usr/bin/env python
# Copyright (c) 2013 Hewlett-Packard Development Company, L.P.
#
# 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.

View File

@ -0,0 +1,77 @@
#!/usr/bin/env python
# Copyright (c) 2013 Hewlett-Packard Development Company, L.P.
#
# 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 re
from flake8 import engine
import pep8
import testtools
from testtools import content
from testtools import content_type
from testtools import matchers
import testscenarios
import hacking
SELFTEST_REGEX = re.compile(r'\b(Okay|[HEW]\d{3}):\s(.*)')
# Each scenario is (name, dict(lines=.., options=..., code=...))
file_cases = []
class HackingTestCase(testtools.TestCase):
scenarios = file_cases
def test_pep8(self):
report = pep8.BaseReport(self.options)
checker = pep8.Checker(lines=self.lines, options=self.options,
report=report)
checker.check_all()
self.addDetail('lines', content.text_content("\n".join(self.lines)))
if self.code == 'Okay':
self.assertThat(
len(report.counters),
matchers.Not(matchers.GreaterThan(
len(self.options.benchmark_keys))),
"incorrectly found %s" % ', '.join(
[key for key in report.counters
if key not in self.options.benchmark_keys]))
else:
self.assertIn(self.code, report.counters)
def _get_lines(check):
for line in check.__doc__.splitlines():
line = line.lstrip()
match = SELFTEST_REGEX.match(line)
if match is None:
continue
yield match.groups()
def load_tests(loader, tests, pattern):
flake8_style = engine.get_style_guide(parse_argv=False, ignore='F')
options = flake8_style.options
for name, check in hacking.__dict__.items():
if not name.startswith("hacking_"):
continue
for (lineno, (code, source)) in enumerate(_get_lines(check)):
lines = [part.replace(r'\t', '\t') + '\n'
for part in source.split(r'\n')]
file_cases.append(("%s-line-%s" % (name, lineno), dict(lines=lines, options=options, code=code)))
return testscenarios.load_tests_apply_scenarios(loader, tests, pattern)

View File

@ -4,4 +4,5 @@ fixtures>=0.3.12
python-subunit python-subunit
sphinx>=1.1.2 sphinx>=1.1.2
testrepository>=0.0.13 testrepository>=0.0.13
testscenarios
testtools>=0.9.27 testtools>=0.9.27