~ubuntu-branches/ubuntu/quantal/nova/quantal-proposed

« back to all changes in this revision

Viewing changes to .pc/0001-Update-tools-hacking-for-pep8-1.2-and-beyond.patch/tools/hacking.py

  • Committer: Package Import Robot
  • Author(s): Adam Gandelman, Adam Gandelman, Chuck Short
  • Date: 2012-08-27 15:37:18 UTC
  • mfrom: (1.1.60)
  • Revision ID: package-import@ubuntu.com-20120827153718-lj8er44eqqz1gsrj
Tags: 2012.2~rc1~20120827.15815-0ubuntu1
[ Adam Gandelman ]
* New upstream release.

[ Chuck Short ]
* debian/patches/0001-Update-tools-hacking-for-pep8-1.2-and-
  beyond.patch: Dropped we dont run pep8 tests anymore.
* debian/control: Drop pep8 build depends
* debian/*.upstart.in: Make sure we transition correctly from runlevel
  1 to 2. (LP: #820694)

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
#!/usr/bin/env python
2
 
# vim: tabstop=4 shiftwidth=4 softtabstop=4
3
 
 
4
 
# Copyright (c) 2012, Cloudscaling
5
 
# All Rights Reserved.
6
 
#
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
10
 
#
11
 
#         http://www.apache.org/licenses/LICENSE-2.0
12
 
#
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
17
 
#    under the License.
18
 
 
19
 
"""nova HACKING file compliance testing
20
 
 
21
 
built on top of pep8.py
22
 
"""
23
 
 
24
 
import fnmatch
25
 
import inspect
26
 
import logging
27
 
import os
28
 
import re
29
 
import subprocess
30
 
import sys
31
 
import tokenize
32
 
import warnings
33
 
 
34
 
import pep8
35
 
 
36
 
# Don't need this for testing
37
 
logging.disable('LOG')
38
 
 
39
 
#N1xx comments
40
 
#N2xx except
41
 
#N3xx imports
42
 
#N4xx docstrings
43
 
#N5xx dictionaries/lists
44
 
#N6xx calling methods
45
 
#N7xx localization
46
 
#N8xx git commit messages
47
 
 
48
 
IMPORT_EXCEPTIONS = ['sqlalchemy', 'migrate', 'nova.db.sqlalchemy.session']
49
 
DOCSTRING_TRIPLE = ['"""', "'''"]
50
 
VERBOSE_MISSING_IMPORT = False
51
 
 
52
 
 
53
 
# Monkey patch broken excluded filter in pep8
54
 
def filename_match(filename, patterns, default=True):
55
 
    """
56
 
    Check if patterns contains a pattern that matches filename.
57
 
    If patterns is unspecified, this always returns True.
58
 
    """
59
 
    if not patterns:
60
 
        return default
61
 
    return any(fnmatch.fnmatch(filename, pattern) for pattern in patterns)
62
 
 
63
 
 
64
 
def excluded(filename):
65
 
    """
66
 
    Check if options.exclude contains a pattern that matches filename.
67
 
    """
68
 
    basename = os.path.basename(filename)
69
 
    return any((filename_match(filename, pep8.options.exclude,
70
 
                               default=False),
71
 
                filename_match(basename, pep8.options.exclude,
72
 
                               default=False)))
73
 
 
74
 
 
75
 
def input_dir(dirname, runner=None):
76
 
    """
77
 
    Check all Python source files in this directory and all subdirectories.
78
 
    """
79
 
    dirname = dirname.rstrip('/')
80
 
    if excluded(dirname):
81
 
        return
82
 
    if runner is None:
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
88
 
        dirs.sort()
89
 
        for subdir in dirs[:]:
90
 
            if excluded(os.path.join(root, subdir)):
91
 
                dirs.remove(subdir)
92
 
        files.sort()
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))
97
 
 
98
 
 
99
 
def is_import_exception(mod):
100
 
    return (mod in IMPORT_EXCEPTIONS or
101
 
            any(mod.startswith(m + '.') for m in IMPORT_EXCEPTIONS))
102
 
 
103
 
 
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])
114
 
    else:
115
 
        return line
116
 
 
117
 
 
118
 
def nova_todo_format(physical_line):
119
 
    """Check for 'TODO()'.
120
 
 
121
 
    nova HACKING guide recommendation for TODO:
122
 
    Include your name with TODOs as in "#TODO(termie)"
123
 
    N101
124
 
    """
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)"
130
 
 
131
 
 
132
 
def nova_except_format(logical_line):
133
 
    """Check for 'except:'.
134
 
 
135
 
    nova HACKING guide recommends not using except:
136
 
    Do not write "except:", use "except Exception:" at the very least
137
 
    N201
138
 
    """
139
 
    if logical_line.startswith("except:"):
140
 
        return 6, "NOVA N201: no 'except:' at least use 'except Exception:'"
141
 
 
142
 
 
143
 
def nova_except_format_assert(logical_line):
144
 
    """Check for 'assertRaises(Exception'.
145
 
 
146
 
    nova HACKING guide recommends not using assertRaises(Exception...):
147
 
    Do not use overly broad Exception type
148
 
    N202
149
 
    """
150
 
    if logical_line.startswith("self.assertRaises(Exception"):
151
 
        return 1, "NOVA N202: assertRaises Exception too broad"
152
 
 
153
 
 
154
 
def nova_one_import_per_line(logical_line):
155
 
    """Check for import format.
156
 
 
157
 
    nova HACKING guide recommends one import per line:
158
 
    Do not import more than one module per line
159
 
 
160
 
    Examples:
161
 
    BAD: from nova.rpc.common import RemoteError, LOG
162
 
    N301
163
 
    """
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"
170
 
 
171
 
_missingImport = set([])
172
 
 
173
 
 
174
 
def nova_import_module_only(logical_line):
175
 
    """Check for import module only.
176
 
 
177
 
    nova HACKING guide recommends importing only modules:
178
 
    Do not import objects, only modules
179
 
    N302 import only modules
180
 
    N303 Invalid Import
181
 
    N304 Relative Import
182
 
    """
183
 
    def importModuleCheck(mod, parent=None, added=False):
184
 
        """
185
 
        If can't find module on first try, recursively check for relative
186
 
        imports
187
 
        """
188
 
        current_path = os.path.dirname(pep8.current_file)
189
 
        try:
190
 
            with warnings.catch_warnings():
191
 
                warnings.simplefilter('ignore', DeprecationWarning)
192
 
                valid = True
193
 
                if parent:
194
 
                    if is_import_exception(parent):
195
 
                        return
196
 
                    parent_mod = __import__(parent, globals(), locals(),
197
 
                        [mod], -1)
198
 
                    valid = inspect.ismodule(getattr(parent_mod, mod))
199
 
                else:
200
 
                    __import__(mod, globals(), locals(), [], -1)
201
 
                    valid = inspect.ismodule(sys.modules[mod])
202
 
                if not valid:
203
 
                    if added:
204
 
                        sys.path.pop()
205
 
                        added = False
206
 
                        return logical_line.find(mod), ("NOVA N304: No "
207
 
                            "relative  imports. '%s' is a relative import"
208
 
                            % logical_line)
209
 
                    return logical_line.find(mod), ("NOVA N302: import only "
210
 
                        "modules. '%s' does not import a module"
211
 
                        % logical_line)
212
 
 
213
 
        except (ImportError, NameError) as exc:
214
 
            if not added:
215
 
                added = True
216
 
                sys.path.append(current_path)
217
 
                return importModuleCheck(mod, parent, added)
218
 
            else:
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" %
223
 
                            (name, exc))
224
 
                    _missingImport.add(name)
225
 
                added = False
226
 
                sys.path.pop()
227
 
                return
228
 
 
229
 
        except AttributeError:
230
 
            # Invalid import
231
 
            return logical_line.find(mod), ("NOVA N303: Invalid import, "
232
 
                "AttributeError raised")
233
 
 
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()
238
 
 
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"))):
242
 
        mod = split_line[1]
243
 
        return importModuleCheck(mod)
244
 
 
245
 
    # TODO(jogo) handle "from x import *"
246
 
 
247
 
#TODO(jogo): import template: N305
248
 
 
249
 
 
250
 
def nova_import_alphabetical(physical_line, line_number, lines):
251
 
    """Check for imports in alphabetical order.
252
 
 
253
 
    nova HACKING guide recommendation for imports:
254
 
    imports in human alphabetical order
255
 
    N306
256
 
    """
257
 
    # handle import x
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"
263
 
    length = [2, 4]
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]))
269
 
 
270
 
 
271
 
def nova_docstring_start_space(physical_line):
272
 
    """Check for docstring not start with space.
273
 
 
274
 
    nova HACKING guide recommendation for docstring:
275
 
    Docstring should not start with space
276
 
    N401
277
 
    """
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"
282
 
                " a space")
283
 
 
284
 
 
285
 
def nova_docstring_one_line(physical_line):
286
 
    """Check one line docstring end.
287
 
 
288
 
    nova HACKING guide recommendation for one line docstring:
289
 
    A one line docstring looks like this and ends in a period.
290
 
    N402
291
 
    """
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"
297
 
 
298
 
 
299
 
def nova_docstring_multiline_end(physical_line):
300
 
    """Check multi line docstring end.
301
 
 
302
 
    nova HACKING guide recommendation for docstring:
303
 
    Docstring should end on a new line
304
 
    N403
305
 
    """
306
 
    pos = max([physical_line.find(i) for i in DOCSTRING_TRIPLE])  # start
307
 
    if (pos != -1 and len(physical_line) == pos):
308
 
        print physical_line
309
 
        if (physical_line[pos + 3] == ' '):
310
 
            return (pos, "NOVA N403: multi line docstring end on new line")
311
 
 
312
 
 
313
 
FORMAT_RE = re.compile("%(?:"
314
 
                            "%|"           # Ignore plain percents
315
 
                            "(\(\w+\))?"   # mapping key
316
 
                            "([#0 +-]?"    # flag
317
 
                             "(?:\d+|\*)?"  # width
318
 
                             "(?:\.\d+)?"   # precision
319
 
                             "[hlL]?"       # length mod
320
 
                             "\w))")        # type
321
 
 
322
 
 
323
 
class LocalizationError(Exception):
324
 
    pass
325
 
 
326
 
 
327
 
def check_i18n():
328
 
    """Generator that checks token stream for localization errors.
329
 
 
330
 
    Expects tokens to be ``send``ed one by one.
331
 
    Raises LocalizationError if some error is found.
332
 
    """
333
 
    while True:
334
 
        try:
335
 
            token_type, text, _, _, line = yield
336
 
        except GeneratorExit:
337
 
            return
338
 
        if (token_type == tokenize.NAME and text == "_" and
339
 
            not line.startswith('def _(msg):')):
340
 
 
341
 
            while True:
342
 
                token_type, text, start, _, _ = yield
343
 
                if token_type != tokenize.NL:
344
 
                    break
345
 
            if token_type != tokenize.OP or text != "(":
346
 
                continue  # not a localization call
347
 
 
348
 
            format_string = ''
349
 
            while True:
350
 
                token_type, text, start, _, _ = yield
351
 
                if token_type == tokenize.STRING:
352
 
                    format_string += eval(text)
353
 
                elif token_type == tokenize.NL:
354
 
                    pass
355
 
                else:
356
 
                    break
357
 
 
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")
364
 
            if text != ")":
365
 
                if text == "%":
366
 
                    raise LocalizationError(start,
367
 
                        "NOVA N702: Formatting operation should be outside"
368
 
                        " of localization method call")
369
 
                elif text == "+":
370
 
                    raise LocalizationError(start,
371
 
                        "NOVA N702: Use bare string concatenation instead"
372
 
                        " of +")
373
 
                else:
374
 
                    raise LocalizationError(start,
375
 
                        "NOVA N702: Argument to _ must be just a string")
376
 
 
377
 
            format_specs = FORMAT_RE.findall(format_string)
378
 
            positional_specs = [(key, spec) for key, spec in format_specs
379
 
                                            if not key and spec]
380
 
            # not spec means %%, key means %(smth)s
381
 
            if len(positional_specs) > 1:
382
 
                raise LocalizationError(start,
383
 
                    "NOVA N703: Multiple positional placeholders")
384
 
 
385
 
 
386
 
def nova_localization_strings(logical_line, tokens):
387
 
    """Check localization in line.
388
 
 
389
 
    N701: bad localization call
390
 
    N702: complex expression instead of string as argument to _()
391
 
    N703: multiple positional placeholders
392
 
    """
393
 
 
394
 
    gen = check_i18n()
395
 
    next(gen)
396
 
    try:
397
 
        map(gen.send, tokens)
398
 
        gen.close()
399
 
    except LocalizationError as e:
400
 
        return e.args
401
 
 
402
 
#TODO(jogo) Dict and list objects
403
 
 
404
 
current_file = ""
405
 
 
406
 
 
407
 
def readlines(filename):
408
 
    """Record the current file being tested."""
409
 
    pep8.current_file = filename
410
 
    return open(filename).readlines()
411
 
 
412
 
 
413
 
def add_nova():
414
 
    """Monkey patch in nova guidelines.
415
 
 
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
419
 
    """
420
 
    for name, function in globals().items():
421
 
        if not inspect.isfunction(function):
422
 
            continue
423
 
        args = inspect.getargspec(function)[0]
424
 
        if args and name.startswith("nova"):
425
 
            exec("pep8.%s = %s" % (name, name))
426
 
 
427
 
 
428
 
def once_git_check_commit_title():
429
 
    """Check git commit messages.
430
 
 
431
 
    nova HACKING recommends not referencing a bug or blueprint in first line,
432
 
    it should provide an accurate description of the change
433
 
    N801
434
 
    N802 Title limited to 50 chars
435
 
    """
436
 
    #Get title of most recent commit
437
 
 
438
 
    subp = subprocess.Popen(['git', 'log', '--pretty=%s', '-1'],
439
 
            stdout=subprocess.PIPE)
440
 
    title = subp.communicate()[0]
441
 
    if subp.returncode:
442
 
        raise Exception("git log failed with code %s" % subp.returncode)
443
 
 
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)
451
 
 
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"
459
 
                % title.strip())
460
 
 
461
 
if __name__ == "__main__":
462
 
    #include nova path
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}')
468
 
    add_nova()
469
 
    pep8.current_file = current_file
470
 
    pep8.readlines = readlines
471
 
    pep8.excluded = excluded
472
 
    pep8.input_dir = input_dir
473
 
    try:
474
 
        pep8._main()
475
 
    finally:
476
 
        if len(_missingImport) > 0:
477
 
            print >> sys.stderr, ("%i imports missing in this test environment"
478
 
                    % len(_missingImport))