summaryrefslogtreecommitdiff
path: root/tools/hacking.py
blob: 716c11541a2e545d1505e5df30318e3c3ddaa0cf (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
#!/usr/bin/env python
# vim: tabstop=4 shiftwidth=4 softtabstop=4

# Copyright (c) 2012, Cloudscaling
# All Rights Reserved.
#
#    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.

"""cloudinit HACKING file compliance testing (based off of nova hacking.py)

built on top of pep8.py
"""

import inspect
import logging
import re
import sys

import pep8

# Don't need this for testing
logging.disable('LOG')

# N1xx comments
# N2xx except
# N3xx imports
# N4xx docstrings
# N[5-9]XX (future use)

DOCSTRING_TRIPLE = ['"""', "'''"]
VERBOSE_MISSING_IMPORT = False
_missingImport = set([])


def import_normalize(line):
    # convert "from x import y" to "import x.y"
    # handle "from x import y as z" to "import x.y as z"
    split_line = line.split()
    if (line.startswith("from ") and "," not in line and
       split_line[2] == "import" and split_line[3] != "*" and
       split_line[1] != "__future__" and
       (len(split_line) == 4 or (len(split_line) == 6 and
                                 split_line[4] == "as"))):
        return "import %s.%s" % (split_line[1], split_line[3])
    else:
        return line


def cloud_import_alphabetical(physical_line, line_number, lines):
    """Check for imports in alphabetical order.

    HACKING guide recommendation for imports:
    imports in human alphabetical order
    N306
    """
    # handle import x
    # use .lower since capitalization shouldn't dictate order
    split_line = import_normalize(physical_line.strip()).lower().split()
    split_previous = import_normalize(lines[line_number - 2])
    split_previous = split_previous.strip().lower().split()
    # with or without "as y"
    length = [2, 4]
    if (len(split_line) in length and len(split_previous) in length and
            split_line[0] == "import" and split_previous[0] == "import"):
        if split_line[1] < split_previous[1]:
            return (0, "N306: imports not in alphabetical order (%s, %s)"
                    % (split_previous[1], split_line[1]))


def cloud_docstring_start_space(physical_line):
    """Check for docstring not start with space.

    HACKING guide recommendation for docstring:
    Docstring should not start with space
    N401
    """
    pos = max([physical_line.find(i) for i in DOCSTRING_TRIPLE])  # start
    if (pos != -1 and len(physical_line) > pos + 1):
        if (physical_line[pos + 3] == ' '):
            return (pos,
                    "N401: one line docstring should not start with a space")


def cloud_todo_format(physical_line):
    """Check for 'TODO()'.

    HACKING guide recommendation for TODO:
    Include your name with TODOs as in "#TODO(termie)"
    N101
    """
    pos = physical_line.find('TODO')
    pos1 = physical_line.find('TODO(')
    pos2 = physical_line.find('#')  # make sure it's a comment
    if (pos != pos1 and pos2 >= 0 and pos2 < pos):
        return pos, "N101: Use TODO(NAME)"


def cloud_docstring_one_line(physical_line):
    """Check one line docstring end.

    HACKING guide recommendation for one line docstring:
    A one line docstring looks like this and ends in a period.
    N402
    """
    pos = max([physical_line.find(i) for i in DOCSTRING_TRIPLE])  # start
    end = max([physical_line[-4:-1] == i for i in DOCSTRING_TRIPLE])  # end
    if (pos != -1 and end and len(physical_line) > pos + 4):
        if (physical_line[-5] != '.'):
            return pos, "N402: one line docstring needs a period"


def cloud_docstring_multiline_end(physical_line):
    """Check multi line docstring end.

    HACKING guide recommendation for docstring:
    Docstring should end on a new line
    N403
    """
    pos = max([physical_line.find(i) for i in DOCSTRING_TRIPLE])  # start
    if (pos != -1 and len(physical_line) == pos):
        print(physical_line)
        if (physical_line[pos + 3] == ' '):
            return (pos, "N403: multi line docstring end on new line")


current_file = ""


def readlines(filename):
    """Record the current file being tested."""
    pep8.current_file = filename
    return open(filename).readlines()


def add_cloud():
    """Monkey patch pep8 for cloud-init guidelines.

    Look for functions that start with cloud_
    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
        if name.startswith("cloud_"):
            exec("pep8.%s = %s" % (name, name))

if __name__ == "__main__":
    # NOVA based 'hacking.py' error codes start with an N
    pep8.ERRORCODE_REGEX = re.compile(r'[EWN]\d{3}')
    add_cloud()
    pep8.current_file = current_file
    pep8.readlines = readlines
    try:
        pep8._main()
    finally:
        if len(_missingImport) > 0:
            print >> sys.stderr, ("%i imports missing in this test environment"
                                  % len(_missingImport))