summaryrefslogtreecommitdiff
path: root/share/spack/qa/flake8_formatter.py
blob: 73aa986af266d18eb43361e0b392147ef30dca02 (plain) (blame)
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
import re
import sys
import pycodestyle
from collections import defaultdict
from flake8.formatting.default import Pylint
from flake8.style_guide import Violation

#: This is a dict that maps:
#:  filename pattern ->
#:     flake8 exemption code ->
#:        list of patterns, for which matching lines should have codes applied.
#:
#: For each file, if the filename pattern matches, we'll add per-line
#: exemptions if any patterns in the sub-dict match.
pattern_exemptions = {
    # exemptions applied only to package.py files.
    r"package.py$": {
        # Allow 'from spack import *' in packages, but no other wildcards
        "F403": [
            r"^from spack import \*$",
            r"^from spack.pkgkit import \*$",
        ],
        # Exempt lines with urls and descriptions from overlong line errors.
        "E501": [
            r"^\s*homepage\s*=",
            r"^\s*url\s*=",
            r"^\s*git\s*=",
            r"^\s*svn\s*=",
            r"^\s*hg\s*=",
            r"^\s*pypi\s*=",
            r"^\s*list_url\s*=",
            r"^\s*version\(",
            r"^\s*variant\(",
            r"^\s*provides\(",
            r"^\s*extends\(",
            r"^\s*depends_on\(",
            r"^\s*conflicts\(",
            r"^\s*resource\(",
            r"^\s*patch\(",
        ],
        # Exempt '@when' decorated functions from redefinition errors.
        "F811": [
            r"^\s*@when\(.*\)",
        ],
    },
    # exemptions applied to all files.
    r".py$": {
        "E501": [
            r"(https?|ftp|file)\:",  # URLs
            r'([\'"])[0-9a-fA-F]{32,}\1',  # long hex checksums
        ]
    },
}


# compile all regular expressions.
pattern_exemptions = dict(
    (
        re.compile(file_pattern),
        dict(
            (code, [re.compile(p) for p in patterns])
            for code, patterns in error_dict.items()
        ),
    )
    for file_pattern, error_dict in pattern_exemptions.items()
)


class SpackFormatter(Pylint):
    def __init__(self, options):
        self.spack_errors = {}
        self.error_seen = False
        super().__init__(options)

    def after_init(self):  # type: () -> None
        """Overriding to keep format string from being unset in Default"""
        pass

    def beginning(self, filename):
        self.filename = filename
        self.file_lines = None
        self.spack_errors = defaultdict(list)
        for file_pattern, errors in pattern_exemptions.items():
            if file_pattern.search(filename):
                for code, pat_arr in errors.items():
                    self.spack_errors[code].extend(pat_arr)

    def handle(self, error):  # type: (Violation) -> None
        """Handle an error reported by Flake8.

        This defaults to calling :meth:`format`, :meth:`show_source`, and
        then :meth:`write`. This version implements the pattern-based ignore
        behavior from `spack flake8` as a native flake8 plugin.

        :param error:
            This will be an instance of
            :class:`~flake8.style_guide.Violation`.
        :type error:
            flake8.style_guide.Violation
        """

        # print(error.code)
        # print(error.physical_line)
        # get list of patterns for this error code
        pats = self.spack_errors.get(error.code, None)
        # if any pattern matches, skip line
        if pats is not None and any(
            (pat.search(error.physical_line) for pat in pats)
        ):
            return

        # Special F811 handling
        # Prior to Python 3.8, `noqa: F811` needed to be placed on the `@when`
        # line
        # Starting with Python 3.8, it must be placed on the `def` line
        # https://gitlab.com/pycqa/flake8/issues/583
        # we can only determine if F811 should be ignored given the previous
        # line, so get the previous line and check it
        if (
            self.spack_errors.get("F811", False)
            and error.code == "F811"
            and error.line_number > 1
        ):
            if self.file_lines is None:
                if self.filename in {"stdin", "-", "(none)", None}:
                    self.file_lines = pycodestyle.stdin_get_value().splitlines(
                        True
                    )
                else:
                    self.file_lines = pycodestyle.readlines(self.filename)
            for pat in self.spack_errors["F811"]:
                if pat.search(self.file_lines[error.line_number - 2]):
                    return

        self.error_seen = True
        line = self.format(error)
        source = self.show_source(error)
        self.write(line, source)

    def stop(self):
        """Override stop to check whether any errors we consider to be errors
        were reported.

        This is a hack, but it makes flake8 behave the desired way.
        """
        if not self.error_seen:
            sys.exit(0)