PylintLinter   A
last analyzed

Complexity

Total Complexity 2

Size/Duplication

Total Lines 21
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
dl 0
loc 21
rs 10
c 0
b 0
f 0
wmc 2

1 Method

Rating   Name   Duplication   Size   Complexity  
A extra_processing() 0 5 2
1
# -*- coding: utf-8 -*-
2
# -----------------------------------------------------------------------------
3
# Copyright (c) 2016 Continuum Analytics, Inc.
4
#
5
# Licensed under the terms of the MIT License
6
# (see LICENSE.txt for details)
7
# -----------------------------------------------------------------------------
8
"""Generic and custom code linters."""
9
10
# Standard library imports
11
import json
12
import os
13
import re
14
15
# Local imports
16
from ciocheck.tools import Tool
17
from ciocheck.utils import run_command
18
19
20
class Linter(Tool):
21
    """Generic linter with json and regex output support."""
22
23
    # Regex matching
24
    pattern = None
25
26
    # Json matching
27
    json_keys = []  # ((old_key, new_key), ...)
28
    output_on_stderr = False
29
30
    def __init__(self, cmd_root):
31
        """Generic linter with json and regex output support."""
32
        super(Linter, self).__init__(cmd_root)
33
        self.paths = None
34
        self.regex = None
35
36
    def _parse_regex(self, string):
37
        """Parse output with grouped regex."""
38
        results = []
39
        self.regex = re.compile(self.pattern, re.VERBOSE)
40
        for matches in self.regex.finditer(string):
41
            results.append(matches.groupdict())
42
        return results
43
44
    def _parse_json(self, string):
45
        """Parse output with json keys."""
46
        data = json.loads(string)
47
        results = []
48
        for item in data:
49
            new_item = {}
50
            for (old_key, new_key) in self.json_keys:
51
                new_item[new_key] = item.pop(old_key)
52
            new_item.update(item)
53
            results.append(new_item)
54
        return results
55
56
    def _parse(self, string):
57
        """Parse linter output."""
58
        if self.json_keys:
59
            results = self._parse_json(string)
60
        elif self.pattern:
61
            results = self._parse_regex(string)
62
        else:
63
            raise Exception('Either a pattern or a json key mapping has to '
64
                            'be defined.')
65
        return results
66
67
    def extra_processing(self, results):
68
        """Override in case extra processing on results is needed."""
69
        return results
70
71
    def run(self, paths):
72
        """Run linter and return a list of dicts."""
73
        self.paths = list(paths.keys()) if isinstance(paths, dict) else paths
74
        if self.paths:
75
            args = list(self.command)
76
            args += self.paths
77
            out, err = run_command(args)
78
            if self.output_on_stderr:
79
                string = err
80
            else:
81
                string = out
82
            results = self._parse(string)
83
            results = self.extra_processing(results)
84
        else:
85
            results = []
86
87
        return results
88
89
90
class Flake8Linter(Linter):
91
    """Flake8 python tool runner."""
92
93
    language = 'python'
94
    name = 'flake8'
95
    extensions = ('py', )
96
    command = ('flake8', )
97
    config_file = '.flake8'
98
    config_sections = [('flake8', 'flake8')]
99
100
    # Match lines of the form:
101
    # path/to/file.py:328: undefined name '_thing'
102
    pattern = r'''
103
        (?P<path>.*?):(?P<line>\d{1,1000}):
104
        (?P<column>\d{1,1000}):\s
105
        (?P<type>[EWFCNTIBDSQ]\d{3})\s
106
        (?P<message>.*)
107
        '''
108
109
110
class Pep8Linter(Linter):
111
    """Pep8 python tool runner."""
112
113
    language = 'python'
114
    name = 'pep8'
115
    extensions = ('py', )
116
    command = ('pep8', )
117
    config_file = '.pep8'
118
    config_sections = [('pep8', 'pep8')]
119
120
    # Match lines of the form:
121
    pattern = r'''
122
        (?P<path>.*?):(?P<line>\d{1,1000}):
123
        (?P<column>\d{1,1000}):\s
124
        (?P<type>[EWFCNTIBDSQ]\d{3})\s
125
        (?P<message>.*)
126
        '''
127
128
129
class PydocstyleLinter(Linter):
130
    """Pydocstyle python tool runner."""
131
132
    language = 'python'
133
    name = 'pydocstyle'
134
    extensions = ('py', )
135
    command = ('pydocstyle', )
136
    config_file = '.pydocstyle'
137
    config_sections = [('pydocstyle', 'pydocstyle')]
138
    output_on_stderr = True
139
140
    # Match lines of the form:
141
    # ./bootstrap.py:1 at module level:
142
    #    D400: First line should end with a period (not 't')
143
    pattern = r'''
144
        (?P<path>.*?):
145
        (?P<line>\d{1,1000000})\  # 1 million lines of code :-p ?
146
        (?P<symbol>.*):\n.*?
147
        (?P<type>D\d{3}):\s
148
        (?P<message>.*)
149
        '''
150
151
152
class PylintLinter(Linter):
153
    """Pylint python tool runner."""
154
155
    language = 'python'
156
    name = 'pylint'
157
    extensions = ('py', )
158
    command = ('pylint', '--output-format', 'json', '-j', '0')
159
    config_file = '.pydocstyle'
160
    config_sections = [('pydocstyle', 'pydocstyle')]
161
    json_keys = (
162
        ('message', 'message'),
163
        ('line', 'line'),
164
        ('column', 'column'),
165
        ('type', 'type'),
166
        ('path', 'path'), )
167
168
    def extra_processing(self, results):
169
        """Make path an absolute path."""
170
        for item in results:
171
            item['path'] = os.path.join(self.cmd_root, item['path'])
172
        return results
173
174
175
LINTERS = [
176
    Pep8Linter,
177
    PydocstyleLinter,
178
    Flake8Linter,
179
    PylintLinter,
180
]
181
182
183
def test():
184
    """Main local test."""
185
    here = os.path.dirname(os.path.realpath(__file__))
186
    paths = [here]
187
    linter = PylintLinter(here)
188
    results = linter.run(paths)
189
    for result in results:
190
        print(result)
191
192
193
if __name__ == '__main__':
194
    test()
195