2
# vim: tabstop=4 shiftwidth=4 softtabstop=4
4
# Copyright (c) 2012, Cloudscaling
7
# Licensed under the Apache License, Version 2.0 (the "License"); you may
8
# not use this file except in compliance with the License. You may obtain
9
# a copy of the License at
11
# http://www.apache.org/licenses/LICENSE-2.0
13
# Unless required by applicable law or agreed to in writing, software
14
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
15
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
16
# License for the specific language governing permissions and limitations
19
"""nova HACKING file compliance testing
21
built on top of pep8.py
36
# Don't need this for testing
37
logging.disable('LOG')
43
#N5xx dictionaries/lists
46
#N8xx git commit messages
48
IMPORT_EXCEPTIONS = ['sqlalchemy', 'migrate', 'nova.db.sqlalchemy.session']
49
DOCSTRING_TRIPLE = ['"""', "'''"]
50
VERBOSE_MISSING_IMPORT = False
53
# Monkey patch broken excluded filter in pep8
54
def filename_match(filename, patterns, default=True):
56
Check if patterns contains a pattern that matches filename.
57
If patterns is unspecified, this always returns True.
61
return any(fnmatch.fnmatch(filename, pattern) for pattern in patterns)
64
def excluded(filename):
66
Check if options.exclude contains a pattern that matches filename.
68
basename = os.path.basename(filename)
69
return any((filename_match(filename, pep8.options.exclude,
71
filename_match(basename, pep8.options.exclude,
75
def input_dir(dirname, runner=None):
77
Check all Python source files in this directory and all subdirectories.
79
dirname = dirname.rstrip('/')
83
runner = pep8.input_file
84
for root, dirs, files in os.walk(dirname):
85
if pep8.options.verbose:
86
print('directory ' + root)
87
pep8.options.counters['directories'] += 1
89
for subdir in dirs[:]:
90
if excluded(os.path.join(root, subdir)):
93
for filename in files:
94
if pep8.filename_match(filename) and not excluded(filename):
95
pep8.options.counters['files'] += 1
96
runner(os.path.join(root, filename))
99
def is_import_exception(mod):
100
return (mod in IMPORT_EXCEPTIONS or
101
any(mod.startswith(m + '.') for m in IMPORT_EXCEPTIONS))
104
def import_normalize(line):
105
# convert "from x import y" to "import x.y"
106
# handle "from x import y as z" to "import x.y as z"
107
split_line = line.split()
108
if (line.startswith("from ") and "," not in line and
109
split_line[2] == "import" and split_line[3] != "*" and
110
split_line[1] != "__future__" and
111
(len(split_line) == 4 or
112
(len(split_line) == 6 and split_line[4] == "as"))):
113
return "import %s.%s" % (split_line[1], split_line[3])
118
def nova_todo_format(physical_line):
119
"""Check for 'TODO()'.
121
nova HACKING guide recommendation for TODO:
122
Include your name with TODOs as in "#TODO(termie)"
125
pos = physical_line.find('TODO')
126
pos1 = physical_line.find('TODO(')
127
pos2 = physical_line.find('#') # make sure it's a comment
128
if (pos != pos1 and pos2 >= 0 and pos2 < pos):
129
return pos, "NOVA N101: Use TODO(NAME)"
132
def nova_except_format(logical_line):
133
"""Check for 'except:'.
135
nova HACKING guide recommends not using except:
136
Do not write "except:", use "except Exception:" at the very least
139
if logical_line.startswith("except:"):
140
return 6, "NOVA N201: no 'except:' at least use 'except Exception:'"
143
def nova_except_format_assert(logical_line):
144
"""Check for 'assertRaises(Exception'.
146
nova HACKING guide recommends not using assertRaises(Exception...):
147
Do not use overly broad Exception type
150
if logical_line.startswith("self.assertRaises(Exception"):
151
return 1, "NOVA N202: assertRaises Exception too broad"
154
def nova_one_import_per_line(logical_line):
155
"""Check for import format.
157
nova HACKING guide recommends one import per line:
158
Do not import more than one module per line
161
BAD: from nova.rpc.common import RemoteError, LOG
164
pos = logical_line.find(',')
165
parts = logical_line.split()
166
if (pos > -1 and (parts[0] == "import" or
167
parts[0] == "from" and parts[2] == "import") and
168
not is_import_exception(parts[1])):
169
return pos, "NOVA N301: one import per line"
171
_missingImport = set([])
174
def nova_import_module_only(logical_line):
175
"""Check for import module only.
177
nova HACKING guide recommends importing only modules:
178
Do not import objects, only modules
179
N302 import only modules
183
def importModuleCheck(mod, parent=None, added=False):
185
If can't find module on first try, recursively check for relative
188
current_path = os.path.dirname(pep8.current_file)
190
with warnings.catch_warnings():
191
warnings.simplefilter('ignore', DeprecationWarning)
194
if is_import_exception(parent):
196
parent_mod = __import__(parent, globals(), locals(),
198
valid = inspect.ismodule(getattr(parent_mod, mod))
200
__import__(mod, globals(), locals(), [], -1)
201
valid = inspect.ismodule(sys.modules[mod])
206
return logical_line.find(mod), ("NOVA N304: No "
207
"relative imports. '%s' is a relative import"
209
return logical_line.find(mod), ("NOVA N302: import only "
210
"modules. '%s' does not import a module"
213
except (ImportError, NameError) as exc:
216
sys.path.append(current_path)
217
return importModuleCheck(mod, parent, added)
219
name = logical_line.split()[1]
220
if name not in _missingImport:
221
if VERBOSE_MISSING_IMPORT:
222
print >> sys.stderr, ("ERROR: import '%s' failed: %s" %
224
_missingImport.add(name)
229
except AttributeError:
231
return logical_line.find(mod), ("NOVA N303: Invalid import, "
232
"AttributeError raised")
234
# convert "from x import y" to " import x.y"
235
# convert "from x import y as z" to " import x.y"
236
import_normalize(logical_line)
237
split_line = logical_line.split()
239
if (logical_line.startswith("import ") and "," not in logical_line and
240
(len(split_line) == 2 or
241
(len(split_line) == 4 and split_line[2] == "as"))):
243
return importModuleCheck(mod)
245
# TODO(jogo) handle "from x import *"
247
#TODO(jogo): import template: N305
250
def nova_import_alphabetical(physical_line, line_number, lines):
251
"""Check for imports in alphabetical order.
253
nova HACKING guide recommendation for imports:
254
imports in human alphabetical order
258
# use .lower since capitalization shouldn't dictate order
259
split_line = import_normalize(physical_line.strip()).lower().split()
260
split_previous = import_normalize(lines[line_number - 2]
261
).strip().lower().split()
262
# with or without "as y"
264
if (len(split_line) in length and len(split_previous) in length and
265
split_line[0] == "import" and split_previous[0] == "import"):
266
if split_line[1] < split_previous[1]:
267
return (0, "NOVA N306: imports not in alphabetical order (%s, %s)"
268
% (split_previous[1], split_line[1]))
271
def nova_docstring_start_space(physical_line):
272
"""Check for docstring not start with space.
274
nova HACKING guide recommendation for docstring:
275
Docstring should not start with space
278
pos = max([physical_line.find(i) for i in DOCSTRING_TRIPLE]) # start
279
if (pos != -1 and len(physical_line) > pos + 1):
280
if (physical_line[pos + 3] == ' '):
281
return (pos, "NOVA N401: one line docstring should not start with"
285
def nova_docstring_one_line(physical_line):
286
"""Check one line docstring end.
288
nova HACKING guide recommendation for one line docstring:
289
A one line docstring looks like this and ends in a period.
292
pos = max([physical_line.find(i) for i in DOCSTRING_TRIPLE]) # start
293
end = max([physical_line[-4:-1] == i for i in DOCSTRING_TRIPLE]) # end
294
if (pos != -1 and end and len(physical_line) > pos + 4):
295
if (physical_line[-5] != '.'):
296
return pos, "NOVA N402: one line docstring needs a period"
299
def nova_docstring_multiline_end(physical_line):
300
"""Check multi line docstring end.
302
nova HACKING guide recommendation for docstring:
303
Docstring should end on a new line
306
pos = max([physical_line.find(i) for i in DOCSTRING_TRIPLE]) # start
307
if (pos != -1 and len(physical_line) == pos):
309
if (physical_line[pos + 3] == ' '):
310
return (pos, "NOVA N403: multi line docstring end on new line")
313
FORMAT_RE = re.compile("%(?:"
314
"%|" # Ignore plain percents
315
"(\(\w+\))?" # mapping key
317
"(?:\d+|\*)?" # width
318
"(?:\.\d+)?" # precision
319
"[hlL]?" # length mod
323
class LocalizationError(Exception):
328
"""Generator that checks token stream for localization errors.
330
Expects tokens to be ``send``ed one by one.
331
Raises LocalizationError if some error is found.
335
token_type, text, _, _, line = yield
336
except GeneratorExit:
338
if (token_type == tokenize.NAME and text == "_" and
339
not line.startswith('def _(msg):')):
342
token_type, text, start, _, _ = yield
343
if token_type != tokenize.NL:
345
if token_type != tokenize.OP or text != "(":
346
continue # not a localization call
350
token_type, text, start, _, _ = yield
351
if token_type == tokenize.STRING:
352
format_string += eval(text)
353
elif token_type == tokenize.NL:
358
if not format_string:
359
raise LocalizationError(start,
360
"NOVA N701: Empty localization string")
361
if token_type != tokenize.OP:
362
raise LocalizationError(start,
363
"NOVA N701: Invalid localization call")
366
raise LocalizationError(start,
367
"NOVA N702: Formatting operation should be outside"
368
" of localization method call")
370
raise LocalizationError(start,
371
"NOVA N702: Use bare string concatenation instead"
374
raise LocalizationError(start,
375
"NOVA N702: Argument to _ must be just a string")
377
format_specs = FORMAT_RE.findall(format_string)
378
positional_specs = [(key, spec) for key, spec in format_specs
380
# not spec means %%, key means %(smth)s
381
if len(positional_specs) > 1:
382
raise LocalizationError(start,
383
"NOVA N703: Multiple positional placeholders")
386
def nova_localization_strings(logical_line, tokens):
387
"""Check localization in line.
389
N701: bad localization call
390
N702: complex expression instead of string as argument to _()
391
N703: multiple positional placeholders
397
map(gen.send, tokens)
399
except LocalizationError as e:
402
#TODO(jogo) Dict and list objects
407
def readlines(filename):
408
"""Record the current file being tested."""
409
pep8.current_file = filename
410
return open(filename).readlines()
414
"""Monkey patch in nova guidelines.
416
Look for functions that start with nova_ and have arguments
417
and add them to pep8 module
418
Assumes you know how to write pep8.py checks
420
for name, function in globals().items():
421
if not inspect.isfunction(function):
423
args = inspect.getargspec(function)[0]
424
if args and name.startswith("nova"):
425
exec("pep8.%s = %s" % (name, name))
428
def once_git_check_commit_title():
429
"""Check git commit messages.
431
nova HACKING recommends not referencing a bug or blueprint in first line,
432
it should provide an accurate description of the change
434
N802 Title limited to 50 chars
436
#Get title of most recent commit
438
subp = subprocess.Popen(['git', 'log', '--pretty=%s', '-1'],
439
stdout=subprocess.PIPE)
440
title = subp.communicate()[0]
442
raise Exception("git log failed with code %s" % subp.returncode)
444
#From https://github.com/openstack/openstack-ci-puppet
445
# /blob/master/modules/gerrit/manifests/init.pp#L74
446
#Changeid|bug|blueprint
447
git_keywords = (r'(I[0-9a-f]{8,40})|'
448
'([Bb]ug|[Ll][Pp])[\s\#:]*(\d+)|'
449
'([Bb]lue[Pp]rint|[Bb][Pp])[\s\#:]*([A-Za-z0-9\\-]+)')
450
GIT_REGEX = re.compile(git_keywords)
452
#NOTE(jogo) if match regex but over 3 words, acceptable title
453
if GIT_REGEX.search(title) is not None and len(title.split()) <= 3:
454
print ("N801: git commit title ('%s') should provide an accurate "
455
"description of the change, not just a reference to a bug "
456
"or blueprint" % title.strip())
457
if len(title.decode('utf-8')) > 72:
458
print ("N802: git commit title ('%s') should be under 50 chars"
461
if __name__ == "__main__":
463
sys.path.append(os.getcwd())
464
#Run once tests (not per line)
465
once_git_check_commit_title()
466
#NOVA error codes start with an N
467
pep8.ERRORCODE_REGEX = re.compile(r'[EWN]\d{3}')
469
pep8.current_file = current_file
470
pep8.readlines = readlines
471
pep8.excluded = excluded
472
pep8.input_dir = input_dir
476
if len(_missingImport) > 0:
477
print >> sys.stderr, ("%i imports missing in this test environment"
478
% len(_missingImport))