Runner   F
last analyzed

Complexity

Total Complexity 65

Size/Duplication

Total Lines 271
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
dl 0
loc 271
rs 3.3333
c 1
b 0
f 0
wmc 65

6 Methods

Rating   Name   Duplication   Size   Complexity  
A __init__() 0 21 1
F process_results() 0 101 31
A format_diff() 0 7 2
B enforce_checks() 0 22 6
A clean() 0 9 3
F run() 0 103 22

How to fix   Complexity   

Complex Class

Complex classes like Runner often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

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
"""CLI Parser for `ciocheck`."""
9
10
# Standard library imports
11
from collections import OrderedDict
12
import argparse
13
import os
14
import shutil
15
import sys
16
17
# Local imports
18
from ciocheck.config import ALL_FILES, load_config
19
from ciocheck.files import FileManager
20
from ciocheck.formatters import FORMATTERS, MULTI_FORMATTERS, MultiFormatter
21
from ciocheck.linters import LINTERS
22
from ciocheck.tools import TOOLS
23
24
25
class Runner(object):
26
    """Main tool runner."""
27
28
    def __init__(self, cmd_root, cli_args, folders=None, files=None):
29
        """Main tool runner."""
30
        # Run options
31
        self.cmd_root = cmd_root  # Folder on which the command was executed
32
        self.config = load_config(cmd_root, cli_args)
33
        self.file_manager = FileManager(folders=folders, files=files)
34
        self.folders = folders
35
        self.files = files
36
        self.all_results = OrderedDict()
37
        self.all_tools = {}
38
        self.test_results = None
39
        self.failed_checks = set()
40
41
        self.check = self.config.get_value('check')
42
        self.enforce = self.config.get_value('enforce')
43
        self.diff_mode = self.config.get_value('diff_mode')
44
        self.file_mode = self.config.get_value('file_mode')
45
        self.branch = self.config.get_value('branch')
46
        self.disable_formatters = cli_args.disable_formatters
47
        self.disable_linters = cli_args.disable_linters
48
        self.disable_tests = cli_args.disable_tests
49
50
    def run(self):
51
        """Run tools."""
52
        msg = 'Running ciocheck'
53
        print('')
54
        print('=' * len(msg))
55
        print(msg)
56
        print('=' * len(msg))
57
        print('')
58
        self.clean()
59
60
        check_linters = [l for l in LINTERS if l.name in self.check]
61
        check_formatters = [f for f in FORMATTERS if f.name in self.check]
62
        check_testers = [t for t in TOOLS if t.name in self.check]
63
        run_multi = any(f for f in MULTI_FORMATTERS if f.name in self.check)
64
65
        # Format before lint, linters may complain about bad formatting
66
67
        # Formatters
68
        if not self.disable_formatters:
69
            for formatter in check_formatters:
70
                print('Running "{}" ...'.format(formatter.name))
71
                tool = formatter(self.cmd_root)
72
                files = self.file_manager.get_files(
73
                    branch=self.branch,
74
                    diff_mode=self.diff_mode,
75
                    file_mode=self.file_mode,
76
                    extensions=tool.extensions)
77
                tool.create_config(self.config)
78
                self.all_tools[tool.name] = tool
79
                results = tool.run(files)
80
                # Pyformat might include files in results that are not in files
81
                # like when an init is created
82
                if results:
83
                    self.all_results[tool.name] = {
84
                        'files': files,
85
                        'results': results,
86
                    }
87
88
            # The result of the the multi formatter is special!
89
            if run_multi:
90
                print('Running "Multi formatter"')
91
                tool = MultiFormatter(self.cmd_root, self.check)
92
                files = self.file_manager.get_files(
93
                    branch=self.branch,
94
                    diff_mode=self.diff_mode,
95
                    file_mode=self.file_mode,
96
                    extensions=tool.extensions)
97
                multi_results = tool.run(files)
98
                for key, values in multi_results.items():
99
                    self.all_results[key] = {
100
                        'files': files,
101
                        'results': values,
102
                    }
103
104
        # Linters
105
        if not self.disable_linters:
106
            for linter in check_linters:
107
                print('Running "{}" ...'.format(linter.name))
108
                tool = linter(self.cmd_root)
109
                files = self.file_manager.get_files(
110
                    branch=self.branch,
111
                    diff_mode=self.diff_mode,
112
                    file_mode=self.file_mode,
113
                    extensions=tool.extensions)
114
                self.all_tools[tool.name] = tool
115
                tool.create_config(self.config)
116
                self.all_results[tool.name] = {
117
                    'files': files,
118
                    'results': tool.run(files),
119
                }
120
121
        # Tests
122
        if not self.disable_tests:
123
            for tester in check_testers:
124
                print('Running "{}" ...'.format(tester.name))
125
                tool = tester(self.cmd_root)
126
                tool.create_config(self.config)
127
                self.all_tools[tool.name] = tool
128
129
                if tool.name == 'pytest':
130
                    tool.setup_pytest_coverage_args(self.folders)
131
132
                files = self.file_manager.get_files(
133
                    branch=self.branch,
134
                    diff_mode=self.diff_mode,
135
                    file_mode=ALL_FILES,
136
                    extensions=tool.extensions)
137
                results = tool.run(files)
138
                if results:
139
                    results['files'] = files
140
                    self.test_results = results
141
142
        for tool in LINTERS + FORMATTERS + TOOLS:
143
            tool.remove_config(self.cmd_root)
144
        self.clean()
145
146
        self.process_results(self.all_results)
147
        if self.enforce_checks():
148
            msg = 'Ciocheck successful run'
149
            print('\n\n' + '=' * len(msg))
150
            print(msg)
151
            print('=' * len(msg))
152
            print('')
153
154
    def process_results(self, all_results):
155
        """Group all results by file path."""
156
        all_changed_paths = []
157
        for tool_name, data in all_results.items():
158
            if data:
159
                files, results = data['files'], data['results']
160
                all_changed_paths += [result['path'] for result in results]
161
162
        all_changed_paths = list(sorted(set(all_changed_paths)))
163
164
        if self.test_results:
165
            test_files = self.test_results.get('files')
166
            test_coverage = self.test_results.get('coverage')
167
        else:
168
            test_files = []
169
            test_coverage = []
170
171
        for path in all_changed_paths:
172
            short_path = path.replace(self.cmd_root, '...')
173
            print('')
174
            print(short_path)
175
            print('-' * len(short_path))
176
            for tool_name, data in all_results.items():
177
                if data:
178
                    files, results = data['files'], data['results']
179
                    lines = [[-1], range(100000)]
180
181
                    if isinstance(files, dict):
182
                        added_lines = files.get(path, lines)[-1]
183
                    else:
184
                        added_lines = lines[-1]
185
186
                    messages = []
187
                    for result in results:
188
                        res_path = result['path']
189
                        if path == res_path:
190
                            # LINTERS
191
                            line = int(result.get('line', -1))
192
                            created = result.get('created')
193
                            added_copy = result.get('added-copy')
194
                            added_header = result.get('added-header')
195
                            diff = result.get('diff')
196
                            if line and line in list(added_lines):
197
                                spaces = (8 - len(str(line))) * ' '
198
                                args = result.copy()
199
                                args['spaces'] = spaces
200
                                msg = ('    {line}:{spaces}'
201
                                       '{type}: {message}').format(**args)
202
                                messages.append(msg)
203
204
                            # Formatters
205
                            if created:
206
                                msg = '    __init__ file created.'
207
                                messages.append(msg)
208
                            if added_copy:
209
                                msg = '    added copyright.'
210
                                messages.append(msg)
211
                            if added_header:
212
                                msg = '    added header.'
213
                                messages.append(msg)
214
                            if diff:
215
                                msg = self.format_diff(diff)
216
                                messages.append(msg)
217
218
                            # TESTERS / COVERAGE
219
220
                    test = [r['path'] for r in results if path == r['path']]
221
                    if test and messages:
222
                        print('\n  ' + tool_name)
223
                        print('  ' + '-' * len(tool_name))
224
                        self.failed_checks.add(tool_name)
225
                        for message in messages:
226
                            print(message)
227
228
            if isinstance(test_files, dict) and test_files:
229
                # Asked for lines changed
230
                if test_coverage:
231
                    lines_changed_not_covered = []
232
                    lines = test_files.get(path)
233
                    lines_added = lines[-1] if lines else []
234
                    lines_covered = test_coverage.get(path)
235
                    for line in lines_added:
236
                        if line not in lines_covered:
237
                            lines_changed_not_covered.append(str(line))
238
239
                    if lines_changed_not_covered:
240
                        uncov_perc = ((1.0 * len(lines_changed_not_covered)) /
241
                                      (1.0 * len(lines_added)))
242
                        cov_perc = (1 - uncov_perc) * 100
243
                        tool_name = 'coverage'
244
                        print('\n  ' + tool_name)
245
                        print('  ' + '-' * len(tool_name))
246
                        print('    The following lines changed and are not '
247
                              'covered by tests ({0}%):'.format(cov_perc))
248
                        print('    ' + ', '.join(lines_changed_not_covered))
249
250
        print('')
251
        pytest_tool = self.all_tools.get('pytest')
252
        if pytest_tool:
253
            if pytest_tool.coverage_fail:
254
                self.failed_checks.add('coverage')
255
256
    def enforce_checks(self):
257
        """Check that enforced checks did not generate reports."""
258
        if self.test_results:
259
            if 'pytest' in self.test_results:
260
                test_summary = self.test_results['pytest']['report']['summary']
261
                if test_summary.get('failed'):
262
                    self.failed_checks.add('pytest')
263
            else:
264
                self.failed_checks.add('pytest')
265
266
        for enforce_tool in self.enforce:
267
            if enforce_tool in self.failed_checks:
268
                msg = "Ciocheck failures in: {0}".format(
269
                    repr(self.failed_checks))
270
                print('\n\n' + '=' * len(msg))
271
                print(msg)
272
                print('=' * len(msg))
273
                print('')
274
                sys.exit(1)
275
                break
276
277
        return True
278
279
    def format_diff(self, diff, indent='    '):
280
        """Format diff to include an indentation for console printing."""
281
        lines = diff.split('\n')
282
        new_lines = []
283
        for line in lines:
284
            new_lines.append(indent + line)
285
        return '\n'.join(new_lines)
286
287
    def clean(self):
288
        """Remove build directories and temporal config files."""
289
        # Clean up leftover trash as best we can
290
        build_tmp = os.path.join(self.cmd_root, 'build', 'tmp')
291
        if os.path.isdir(build_tmp):
292
            try:
293
                shutil.rmtree(build_tmp, ignore_errors=True)
294
            except Exception:
295
                pass
296
297
298
def main():
299
    """CLI `Parser for ciocheck`."""
300
    description = 'Run Continuum IO test suite.'
301
    parser = argparse.ArgumentParser(description=description)
302
    parser.add_argument(
303
        'folders', help='Folders to analyze. Use from repo root.', nargs='+')
304
    parser.add_argument(
305
        '--disable-formatters',
306
        '-df',
307
        action='store_true',
308
        default=False,
309
        help=('Skip all configured formatters'))
310
    parser.add_argument(
311
        '--disable-linters',
312
        '-dl',
313
        action='store_true',
314
        default=False,
315
        help=('Skip all configured linters'))
316
    parser.add_argument(
317
        '--disable-tests',
318
        '-dt',
319
        action='store_true',
320
        default=False,
321
        help=('Skip running tests'))
322
    parser.add_argument(
323
        '--file-mode',
324
        '-fm',
325
        dest='file_mode',
326
        choices=['lines', 'files', 'all'],
327
        default=None,
328
        help=('Define if the tool should run on modified '
329
              'lines of files (default), modified files or '
330
              'all files'))
331
    parser.add_argument(
332
        '--diff-mode',
333
        '-dm',
334
        dest='diff_mode',
335
        choices=['commited', 'staged', 'unstaged'],
336
        default=None,
337
        help='Define diff mode. Default mode is commited.')
338
    parser.add_argument(
339
        '--branch',
340
        '-b',
341
        dest='branch',
342
        default=None,
343
        help=('Define branch to compare to. Default branch is '
344
              '"origin/master"'))
345
    parser.add_argument(
346
        '--check',
347
        '-c',
348
        dest='check',
349
        nargs='+',
350
        choices=[
351
            'pep8', 'pydocstyle', 'flake8', 'pylint', 'pyformat', 'isort',
352
            'yapf', 'autopep8', 'coverage', 'pytest'
353
        ],
354
        default=None,
355
        help='Select tools to run. Default is "pep8"')
356
    parser.add_argument(
357
        '--enforce',
358
        '-e',
359
        dest='enforce',
360
        choices=[
361
            'pep8', 'pydocstyle', 'flake8', 'pylint', 'pyformat', 'isort',
362
            'yapf', 'autopep8', 'coverage', 'pytest'
363
        ],
364
        default=None,
365
        nargs='+',
366
        help=('Select tools to enforce. Enforced tools will '
367
              'fail if a result is obtained. Default is '
368
              'none.'))
369
    parser.add_argument(
370
        '--config',
371
        '-cf',
372
        dest='config_file',
373
        default=None,
374
        help=('Select a config file to use. Default is none.'))
375
376
    cli_args = parser.parse_args()
377
    root = os.getcwd()
378
    folders = []
379
    files = []
380
    for folder_or_file in cli_args.folders:
381
        folder_or_file = os.path.abspath(folder_or_file)
382
        if os.path.isfile(folder_or_file):
383
            files.append(folder_or_file)
384
        elif os.path.isdir(folder_or_file):
385
            folders.append(folder_or_file)
386
387
    if folders or files:
388
        test = Runner(root, cli_args, folders=folders, files=files)
389
        test.run()
390
    elif not folders and not files:
391
        print('Invalid folders or files!')
392
393
394
if __name__ == '__main__':
395
    main()
396