quality.py 10.5 KB
Newer Older
1 2 3
"""
Check code quality using pep8, pylint, and diff_quality.
"""
4
from paver.easy import sh, task, cmdopts, needs, BuildFailure
5
import os
6 7
import re

8 9
from .utils.envs import Env

10

11 12 13 14
@task
@needs('pavelib.prereqs.install_python_prereqs')
@cmdopts([
    ("system=", "s", "System to act on"),
Christine Lytwynec committed
15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
])
def find_fixme(options):
    """
    Run pylint on system code, only looking for fixme items.
    """
    num_fixme = 0
    systems = getattr(options, 'system', 'lms,cms,common').split(',')

    for system in systems:
        # Directory to put the pylint report in.
        # This makes the folder if it doesn't already exist.
        report_dir = (Env.REPORT_DIR / system).makedirs_p()

        apps = [system]

        for directory in ['djangoapps', 'lib']:
            dirs = os.listdir(os.path.join(system, directory))
            apps.extend([d for d in dirs if os.path.isdir(os.path.join(system, directory, d))])

        apps_list = ' '.join(apps)

        pythonpath_prefix = (
37 38
            "PYTHONPATH={system}:{system}/lib"
            "common/djangoapps:common/lib".format(
Christine Lytwynec committed
39 40 41 42 43 44
                system=system
            )
        )

        sh(
            "{pythonpath_prefix} pylint --disable R,C,W,E --enable=fixme "
David Baumgold committed
45 46
            "--msg-template={msg_template} {apps} "
            "| tee {report_dir}/pylint_fixme.report".format(
Christine Lytwynec committed
47
                pythonpath_prefix=pythonpath_prefix,
David Baumgold committed
48
                msg_template='"{path}:{line}: [{msg_id}({symbol}), {obj}] {msg}"',
Christine Lytwynec committed
49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
                apps=apps_list,
                report_dir=report_dir
            )
        )

        num_fixme += _count_pylint_violations(
            "{report_dir}/pylint_fixme.report".format(report_dir=report_dir))

    print("Number of pylint fixmes: " + str(num_fixme))


@task
@needs('pavelib.prereqs.install_python_prereqs')
@cmdopts([
    ("system=", "s", "System to act on"),
64
    ("errors", "e", "Check for errors only"),
65
    ("limit=", "l", "limit for number of acceptable violations"),
66 67 68
])
def run_pylint(options):
    """
69 70
    Run pylint on system code. When violations limit is passed in,
    fail the task if too many violations are found.
71
    """
72 73
    num_violations = 0
    violations_limit = int(getattr(options, 'limit', -1))
74
    errors = getattr(options, 'errors', False)
75
    systems = getattr(options, 'system', 'lms,cms,common').split(',')
76

77 78 79
    for system in systems:
        # Directory to put the pylint report in.
        # This makes the folder if it doesn't already exist.
80
        report_dir = (Env.REPORT_DIR / system).makedirs_p()
81

82
        flags = []
83 84
        if errors:
            flags.append("--errors-only")
85

86
        apps = [system]
87

88
        for directory in ['lib']:
89 90
            dirs = os.listdir(os.path.join(system, directory))
            apps.extend([d for d in dirs if os.path.isdir(os.path.join(system, directory, d))])
91

92
        apps_list = ' '.join(apps)
93

94 95 96 97 98
        pythonpath_prefix = (
            "PYTHONPATH={system}:{system}/djangoapps:{system}/"
            "lib:common/djangoapps:common/lib".format(
                system=system
            )
99
        )
100 101

        sh(
David Baumgold committed
102
            "{pythonpath_prefix} pylint {flags} --msg-template={msg_template} {apps} | "
103 104
            "tee {report_dir}/pylint.report".format(
                pythonpath_prefix=pythonpath_prefix,
105
                flags=" ".join(flags),
David Baumgold committed
106
                msg_template='"{path}:{line}: [{msg_id}({symbol}), {obj}] {msg}"',
107 108 109
                apps=apps_list,
                report_dir=report_dir
            )
110 111
        )

112 113 114 115 116 117 118 119
        num_violations += _count_pylint_violations(
            "{report_dir}/pylint.report".format(report_dir=report_dir))

    print("Number of pylint violations: " + str(num_violations))
    if num_violations > violations_limit > -1:
        raise Exception("Failed. Too many pylint violations. "
                        "The limit is {violations_limit}.".format(violations_limit=violations_limit))

120

121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137
def _count_pylint_violations(report_file):
    """
    Parses a pylint report line-by-line and determines the number of violations reported
    """
    num_violations_report = 0
    # An example string:
    # common/lib/xmodule/xmodule/tests/test_conditional.py:21: [C0111(missing-docstring), DummySystem] Missing docstring
    # More examples can be found in the unit tests for this method
    pylint_pattern = re.compile(".(\d+):\ \[(\D\d+.+\]).")

    for line in open(report_file):
        violation_list_for_line = pylint_pattern.split(line)
        # If the string is parsed into four parts, then we've found a violation. Example of split parts:
        # test file, line number, violation name, violation details
        if len(violation_list_for_line) == 4:
            num_violations_report += 1
    return num_violations_report
138

139

140 141 142 143
@task
@needs('pavelib.prereqs.install_python_prereqs')
@cmdopts([
    ("system=", "s", "System to act on"),
144
    ("limit=", "l", "limit for number of acceptable violations"),
145 146 147
])
def run_pep8(options):
    """
148 149
    Run pep8 on system code. When violations limit is passed in,
    fail the task if too many violations are found.
150
    """
151
    num_violations = 0
152
    systems = getattr(options, 'system', 'lms,cms,common').split(',')
153
    violations_limit = int(getattr(options, 'limit', -1))
154

155 156 157
    for system in systems:
        # Directory to put the pep8 report in.
        # This makes the folder if it doesn't already exist.
158
        report_dir = (Env.REPORT_DIR / system).makedirs_p()
159

160
        sh('pep8 {system} | tee {report_dir}/pep8.report'.format(system=system, report_dir=report_dir))
161 162 163 164 165 166 167 168
        num_violations = num_violations + _count_pep8_violations(
            "{report_dir}/pep8.report".format(report_dir=report_dir))

    print("Number of pep8 violations: " + str(num_violations))
    # Fail the task if the violations limit has been reached
    if num_violations > violations_limit > -1:
        raise Exception("Failed. Too many pep8 violations. "
                        "The limit is {violations_limit}.".format(violations_limit=violations_limit))
169

170

171 172 173
def _count_pep8_violations(report_file):
    num_lines = sum(1 for line in open(report_file))
    return num_lines
174

175

176 177
@task
@needs('pavelib.prereqs.install_python_prereqs')
178
@cmdopts([
179
    ("compare-branch=", "b", "Branch to compare against, defaults to origin/master"),
180 181 182
    ("percentage=", "p", "fail if diff-quality is below this percentage"),
])
def run_quality(options):
183 184
    """
    Build the html diff quality reports, and print the reports to the console.
185
    :param: b, the branch to compare against, defaults to origin/master
186 187
    :param: p, diff-quality will fail if the quality percentage calculated is
        below this percentage. For example, if p is set to 80, and diff-quality finds
188
        quality of the branch vs the compare branch is less than 80%, then this task will fail.
189
        This threshold would be applied to both pep8 and pylint.
190 191 192 193
    """

    # Directory to put the diff reports in.
    # This makes the folder if it doesn't already exist.
194
    dquality_dir = (Env.REPORT_DIR / "diff_quality").makedirs_p()
195
    diff_quality_percentage_failure = False
196

197 198 199 200 201 202
    # Set the string, if needed, to be used for the diff-quality --compare-branch switch.
    compare_branch = getattr(options, 'compare_branch', None)
    compare_branch_string = ''
    if compare_branch:
        compare_branch_string = '--compare-branch={0}'.format(compare_branch)

203 204
    # Set the string, if needed, to be used for the diff-quality --fail-under switch.
    diff_threshold = int(getattr(options, 'percentage', -1))
205
    percentage_string = ''
206
    if diff_threshold > -1:
207
        percentage_string = '--fail-under={0}'.format(diff_threshold)
208 209

    # Generate diff-quality html report for pep8, and print to console
210 211 212
    # If pep8 reports exist, use those
    # Otherwise, `diff-quality` will call pep8 itself

213
    pep8_files = get_violations_reports("pep8")
214 215
    pep8_reports = u' '.join(pep8_files)

216 217
    try:
        sh(
218
            "diff-quality --violations=pep8 {pep8_reports} {percentage_string} "
219
            "{compare_branch_string} --html-report {dquality_dir}/diff_quality_pep8.html".format(
220 221
                pep8_reports=pep8_reports,
                percentage_string=percentage_string,
222
                compare_branch_string=compare_branch_string,
223 224
                dquality_dir=dquality_dir
            )
225 226 227 228 229 230
        )
    except BuildFailure, error_message:
        if is_percentage_failure(error_message):
            diff_quality_percentage_failure = True
        else:
            raise BuildFailure(error_message)
231

232
    # Generate diff-quality html report for pylint, and print to console
233 234 235
    # If pylint reports exist, use those
    # Otherwise, `diff-quality` will call pylint itself

236
    pylint_files = get_violations_reports("pylint")
237 238 239 240 241 242 243
    pylint_reports = u' '.join(pylint_files)

    pythonpath_prefix = (
        "PYTHONPATH=$PYTHONPATH:lms:lms/djangoapps:lms/lib:cms:cms/djangoapps:cms/lib:"
        "common:common/djangoapps:common/lib"
    )

244 245
    try:
        sh(
246
            "{pythonpath_prefix} diff-quality --violations=pylint "
247
            "{pylint_reports} {percentage_string} {compare_branch_string} "
248
            "--html-report {dquality_dir}/diff_quality_pylint.html ".format(
249 250
                pythonpath_prefix=pythonpath_prefix,
                pylint_reports=pylint_reports,
251
                percentage_string=percentage_string,
252
                compare_branch_string=compare_branch_string,
253
                dquality_dir=dquality_dir,
254
            )
255
        )
256 257 258 259 260 261
    except BuildFailure, error_message:
        if is_percentage_failure(error_message):
            diff_quality_percentage_failure = True
        else:
            raise BuildFailure(error_message)

262
    # If one of the diff-quality runs fails, then paver exits with an error when it is finished
263 264 265 266 267 268 269 270 271 272 273 274 275 276
    if diff_quality_percentage_failure:
        raise BuildFailure("Diff-quality failure(s).")


def is_percentage_failure(error_message):
    """
    When diff-quality is run with a threshold percentage, it ends with an exit code of 1. This bubbles up to
    paver with a subprocess return code error. If the subprocess exits with anything other than 1, raise
    a paver exception.
    """
    if "Subprocess return code: 1" not in error_message:
        return False
    else:
        return True
277 278 279 280 281 282 283 284 285 286 287 288


def get_violations_reports(violations_type):
    """
    Finds violations reports files by naming convention (e.g., all "pep8.report" files)
    """
    violations_files = []
    for subdir, _dirs, files in os.walk(os.path.join(Env.REPORT_DIR)):
        for f in files:
            if f == "{violations_type}.report".format(violations_type=violations_type):
                violations_files.append(os.path.join(subdir, f))
    return violations_files