Completed
Pull Request — master (#1095)
by Lasse
01:37
created

coalib.output.apply_action()   A

Complexity

Conditions 3

Size

Total Lines 47

Duplication

Lines 0
Ratio 0 %
Metric Value
cc 3
dl 0
loc 47
rs 9.0303
1
try:
2
    # This import has side effects and is needed to make input() behave nicely
3
    import readline  # pylint: disable=unused-import
4
except ImportError:  # pragma: no cover
5
    pass
6
7
from pyprint.ConsolePrinter import ConsolePrinter
8
9
from coalib.results.RESULT_SEVERITY import (
10
    RESULT_SEVERITY,
11
    RESULT_SEVERITY_COLORS)
12
from coalib.output.printers.LOG_LEVEL import LOG_LEVEL
13
from coalib.settings.Setting import Setting
14
from coalib.results.Result import Result
15
from coalib.misc.DictUtilities import inverse_dicts
16
from coalib.results.result_actions.OpenEditorAction import OpenEditorAction
17
from coalib.results.result_actions.ApplyPatchAction import ApplyPatchAction
18
from coalib.results.result_actions.PrintDebugMessageAction import (
19
    PrintDebugMessageAction)
20
from coalib.results.result_actions.ShowPatchAction import ShowPatchAction
21
22
23
STR_GET_VAL_FOR_SETTING = ("Please enter a value for the setting \"{}\" ({}) "
24
                           "needed by {}: ")
25
STR_LINE_DOESNT_EXIST = ("The line belonging to the following result "
26
                         "cannot be printed because it refers to a line "
27
                         "that doesn't seem to exist in the given file.")
28
STR_PROJECT_WIDE = "Project wide:"
29
FILE_NAME_COLOR = "blue"
30
FILE_LINES_COLOR = "blue"
31
HIGHLIGHTED_CODE_COLOR = 'red'
32
SUCCESS_COLOR = 'green'
33
CLI_ACTIONS = [OpenEditorAction(),
34
               ApplyPatchAction(),
35
               PrintDebugMessageAction(),
36
               ShowPatchAction()]
37
38
39
def format_lines(lines, line_nr=""):
40
    return '\n'.join("|{:>4}| {}".format(line_nr, line)
41
                     for line in lines.rstrip("\n").split('\n'))
42
43
44
def print_section_beginning(console_printer, section):
45
    """
46
    Will be called after initialization current_section in
47
    begin_section()
48
49
    :param console_printer: Object to print messages on the console.
50
    :param section:         The section that will get executed now.
51
    """
52
    console_printer.print("Executing section {name}...".format(
53
        name=section.name))
54
55
56
def nothing_done(log_printer):
57
    """
58
    Will be called after processing a coafile when nothing had to be done,
59
    i.e. no section was enabled/targeted.
60
61
    :param log_printer: A LogPrinter object.
62
    """
63
    log_printer.warn("No existent section was targeted or enabled. "
64
                     "Nothing to do.")
65
66
67
def print_lines(console_printer,
68
                file_dict,
69
                sourcerange):
70
    """
71
    Prints the lines between the current and the result line. If needed
72
    they will be shortened.
73
74
    :param console_printer: Object to print messages on the console.
75
    :param file_dict:       A dictionary containing all files as values with
76
                            filenames as key.
77
    :param sourcerange:     The SourceRange object referring to the related
78
                            lines to print.
79
    """
80
    for i in range(sourcerange.start.line, sourcerange.end.line + 1):
81
        console_printer.print(format_lines(lines='', line_nr=i),
82
                              color=FILE_LINES_COLOR,
83
                              end='')
84
85
        line = file_dict[sourcerange.file][i - 1].rstrip("\n")
86
        printed_chars = 0
87
        if i == sourcerange.start.line and sourcerange.start.column:
88
            console_printer.print(line[:sourcerange.start.column-1],
89
                                  color=FILE_LINES_COLOR,
90
                                  end='')
91
            printed_chars = sourcerange.start.column-1
92
93
        if i == sourcerange.end.line and sourcerange.end.column:
94
            console_printer.print(line[printed_chars:sourcerange.end.column-1],
95
                                  color=HIGHLIGHTED_CODE_COLOR,
96
                                  end='')
97
            console_printer.print(line[sourcerange.end.column-1:],
98
                                  color=FILE_LINES_COLOR)
99
        else:
100
            console_printer.print(line[printed_chars:],
101
                                  color=HIGHLIGHTED_CODE_COLOR)
102
103
104
def print_result(console_printer,
105
                 log_printer,
106
                 section,
107
                 file_diff_dict,
108
                 result,
109
                 file_dict):
110
    """
111
    Prints the result to console.
112
113
    :param console_printer: Object to print messages on the console.
114
    :param log_printer:     Printer responsible for logging the messages.
115
    :param section:         Name of section to which the result belongs.
116
    :param file_diff_dict:  Dictionary containing filenames as keys and Diff
117
                            objects as values.
118
    :param result:          A derivative of Result.
119
    :param file_dict:       A dictionary containing all files with filename as
120
                            key.
121
    """
122
    if not isinstance(result, Result):
123
        log_printer.warn("One of the results can not be printed since it is "
124
                         "not a valid derivative of the coala result "
125
                         "class.")
126
        return
127
128
    console_printer.print(format_lines("[{sev}] {bear}:".format(
129
        sev=RESULT_SEVERITY.__str__(result.severity), bear=result.origin)),
130
        color=RESULT_SEVERITY_COLORS[result.severity])
131
    console_printer.print(format_lines(result.message), delimiter="\n")
132
133
    actions = []
134
    for action in CLI_ACTIONS:
135
        if action.is_applicable(result, file_dict, file_diff_dict):
136
            actions.append(action)
137
138
    if actions == []:
139
        return
140
141
    action_dict = {}
142
    metadata_list = []
143
    for action in actions:
144
        metadata = action.get_metadata()
145
        action_dict[metadata.name] = action
146
        metadata_list.append(metadata)
147
148
    # User can always choose no action which is guaranteed to succeed
149
    while apply_action(log_printer,
150
                       console_printer,
151
                       section,
152
                       metadata_list,
153
                       action_dict,
154
                       result,
155
                       file_diff_dict,
156
                       file_dict):
157
        pass
158
159
160
def print_results_formatted(log_printer,
161
                            section,
162
                            result_list,
163
                            *args):
164
    format_str = str(section.get(
165
        "format_str",
166
        "id:{id}:origin:{origin}:file:{file}:from_line:{line}:from_column:"
167
        "{column}:to_line:{end_line}:to_column:{end_column}:severity:"
168
        "{severity}:msg:{message}"))
169
    for result in result_list:
170
        try:
171
            if len(result.affected_code) == 0:
172
                print(format_str.format(file=None,
173
                                        line=None,
174
                                        end_line=None,
175
                                        column=None,
176
                                        end_column=None,
177
                                        **result.__dict__))
178
                continue
179
180
            for range in result.affected_code:
181
                print(format_str.format(file=range.start.file,
182
                                        line=range.start.line,
183
                                        end_line=range.end.line,
184
                                        column=range.start.column,
185
                                        end_column=range.end.column,
186
                                        **result.__dict__))
187
        except KeyError as exception:
188
            log_printer.log_exception(
189
                "Unable to print the result with the given format string.",
190
                exception)
191
192
193
def print_results(log_printer,
194
                  section,
195
                  result_list,
196
                  file_dict,
197
                  file_diff_dict,
198
                  color=True):
199
    """
200
    Print all the results in a section.
201
202
    :param log_printer:    Printer responsible for logging the messages.
203
    :param section:        The section to which the results belong to.
204
    :param result_list:    List containing the results
205
    :param file_dict:      A dictionary containing all files with filename as
206
                           key.
207
    :param file_diff_dict: A dictionary that contains filenames as keys and
208
                           diff objects as values.
209
    :param color:          Boolean variable to print the results in color or
210
                           not. Can be used for testing.
211
    """
212
    console_printer = ConsolePrinter(print_colored=color)
213
214
    for result in sorted(result_list):
215
        if len(result.affected_code) == 0:
216
            console_printer.print("\n" + STR_PROJECT_WIDE,
217
                                  color=FILE_NAME_COLOR)
218
        else:
219
            for sourcerange in result.affected_code:
220
                if (
221
                        sourcerange.file is not None and
222
                        sourcerange.file not in file_dict):
223
                    log_printer.warn("The context for the result ({}) cannot "
224
                                     "be printed because it refers to a file "
225
                                     "that doesn't seem to exist ({})"
226
                                     ".".format(str(result), sourcerange.file))
227
                else:
228
                    print_affected_lines(console_printer,
229
                                         file_dict,
230
                                         sourcerange)
231
232
        print_result(console_printer,
233
                     log_printer,
234
                     section,
235
                     file_diff_dict,
236
                     result,
237
                     file_dict)
238
239
240
def print_affected_lines(console_printer, file_dict, sourcerange):
241
    console_printer.print("\n" + sourcerange.file, color=FILE_NAME_COLOR)
242
243
    if sourcerange.start.line is not None:
244
        if len(file_dict[sourcerange.file]) < sourcerange.end.line:
245
            console_printer.print(format_lines(lines=STR_LINE_DOESNT_EXIST))
246
        else:
247
            print_lines(console_printer,
248
                        file_dict,
249
                        sourcerange)
250
251
252
def require_setting(log_printer, setting_name, arr):
253
    """
254
    This method is responsible for prompting a user about a missing setting and
255
    taking its value as input from the user.
256
257
    :param log_printer:  Printer responsible for logging the messages.
258
    :param setting_name: Name od the setting missing
259
    :param arr:          a list containing a description in [0] and the name
260
                         of the bears who need this setting in [1] and
261
                         following.
262
    """
263
    if not isinstance(arr, list) or len(arr) < 2:
264
        log_printer.log(LOG_LEVEL.WARNING,
265
                        "One of the given settings ({}) is not properly "
266
                        "described.".format(str(setting_name)))
267
268
        return None
269
270
    if len(arr) == 2:
271
        needed = arr[1]
272
    else:
273
        needed = ", ".join(arr[1:-1]) + " and " + arr[-1]
274
275
    return input(STR_GET_VAL_FOR_SETTING.format(str(setting_name),
276
                                                str(arr[0]),
277
                                                needed))
278
279
280
def acquire_settings(log_printer, settings_names_dict):
281
    """
282
    This method prompts the user for the given settings.
283
284
    :param log_printer: Printer responsible for logging the messages.
285
    :param settings:    a dictionary with the settings name as key and a list
286
                        containing a description in [0] and the name of the
287
                        bears who need this setting in [1] and following.
288
                     Example:
289
    {"UseTabs": ["describes whether tabs should be used instead of spaces",
290
                 "SpaceConsistencyBear",
291
                 "SomeOtherBear"]}
292
293
    :return:            a dictionary with the settings name as key and the
294
                        given value as value.
295
    """
296
    if not isinstance(settings_names_dict, dict):
297
        raise TypeError("The settings_names_dict parameter has to be a "
298
                        "dictionary.")
299
300
    result = {}
301
    for setting_name, arr in settings_names_dict.items():
302
        value = require_setting(log_printer, setting_name, arr)
303
        if value is not None:
304
            result[setting_name] = value
305
306
    return result
307
308
309
def get_action_info(section, action):
310
    """
311
    Get all the required Settings for an action. It updates the section with
312
    the Settings.
313
314
    :param section: The section the action corresponds to.
315
    :param action:  The action to get the info for.
316
    :return:        Action name and the updated section.
317
    """
318
    params = action.non_optional_params
319
320
    if section is None:
321
        raise ValueError("section has to be intializied.")
322
323
    for param_name in params:
324
        if param_name not in section:
325
            question = format_lines(
326
                "Please enter a value for the parameter '{}' ({}): "
327
                .format(param_name, params[param_name][0]))
328
            section.append(Setting(param_name, input(question)))
329
330
    return action.name, section
331
332
333
def choose_action(console_printer, actions):
334
    """
335
    Presents the actions available to the user and takes as input the action
336
    the user wants to choose.
337
338
    :param console_printer: Object to print messages on the console.
339
    :param actions:         Actions available to the user.
340
    :return:                Return choice of action of user.
341
    """
342
    console_printer.print(format_lines(
343
        "The following actions are applicable to this result:"))
344
345
    while True:
346
        console_printer.print(format_lines(" 0: " +
347
                                           "Apply no further actions."))
348
        for i, action in enumerate(actions):
349
            console_printer.print(format_lines("{:>2}: {}".format(i + 1,
350
                                                                 action.desc)))
351
352
        try:
353
            line = format_lines("Please enter the number of the action "
354
                                "you want to execute. ")
355
            choice = int(input(line))
356
            if 0 <= choice <= len(actions):
357
                return choice
358
        except ValueError:
359
            pass
360
361
        console_printer.print(format_lines("Please enter a valid number."))
362
363
364
def print_actions(console_printer, section, actions):
365
    """
366
    Prints the given actions and lets the user choose.
367
368
    :param actions: A list of FunctionMetadata objects.
369
    :return:        A touple with the name member of the FunctionMetadata
370
                    object chosen by the user and a Section containing at
371
                    least all needed values for the action. If the user did
372
                    choose to do nothing, return (None, None).
373
    """
374
    choice = choose_action(console_printer, actions)
375
376
    if choice == 0:
377
        return None, None
378
379
    return get_action_info(section, actions[choice - 1])
380
381
382
def apply_action(log_printer,
383
                 console_printer,
384
                 section,
385
                 metadata_list,
386
                 action_dict,
387
                 result,
388
                 file_diff_dict,
389
                 file_dict):
390
    """
391
    Applies the action selected by the user.
392
393
    :param log_printer:     Printer responsible for logging the messages.
394
    :param console_printer: Object to print messages on the console.
395
    :param section:         Currently active section.
396
    :param metadata_list:   Contains metadata for all the actions.
397
    :param action_dict:     Contains the action names as keys and their
398
                            references as values.
399
    :param result:          Result corresponding to the actions.
400
    :param file_diff_dict:  If its an action which applies a patch, this
401
                            contains the diff of the patch to be applied to
402
                            the file with filename as keys.
403
    :param file_dict:       Dictionary with filename as keys and its contents
404
                            as values.
405
    :return:                Returns a boolean value. True will be returned, if
406
                            it makes sense that the user may choose to execute
407
                            another action, False otherwise.
408
    """
409
    action_name, section = print_actions(console_printer,
410
                                         section,
411
                                         metadata_list)
412
    if action_name is None:
413
        return False
414
415
    chosen_action = action_dict[action_name]
416
    try:
417
        chosen_action.apply_from_section(result,
418
                                         file_dict,
419
                                         file_diff_dict,
420
                                         section)
421
        console_printer.print(
422
            format_lines("The action was executed successfully."),
423
            color=SUCCESS_COLOR)
424
    except Exception as exception:  # pylint: disable=broad-except
425
        log_printer.log_exception("Failed to execute the action {}.".
426
                                  format(action_name), exception)
427
428
    return True
429
430
431
def show_enumeration(console_printer,
432
                     title,
433
                     items,
434
                     indentation,
435
                     no_items_text):
436
    """
437
    This function takes as input an iterable object (preferably a list or
438
    a dict). And prints in a stylized format. If the iterable object is
439
    empty, it prints a specific statement give by the user. An e.g :
440
441
    <indentation>Title:
442
    <indentation> * Item 1
443
    <indentation> * Item 2
444
445
    :param console_printer: Object to print messages on the console.
446
    :param title:           Title of the text to be printed
447
    :param items:           The iterable object.
448
    :param indentation:     Number of spaces to indent every line by.
449
    :param no_items_text:   Text printed when iterable object is empty.
450
    """
451
    if not items:
452
        console_printer.print(indentation + no_items_text)
453
    else:
454
        console_printer.print(indentation + title)
455
        if isinstance(items, dict):
456
            for key, value in items.items():
457
                console_printer.print(indentation + " * " + key + ": " +
458
                                      value[0])
459
        else:
460
            for item in items:
461
                console_printer.print(indentation + " * " + item)
462
    console_printer.print()
463
464
465
def show_bear(console_printer, bear, sections, metadata):
466
    """
467
    Display all information about a bear.
468
469
    :param console_printer: Object to print messages on the console.
470
    :param bear:            The bear to be displayed.
471
    :param sections:        The sections to which the bear belongs.
472
    :param metadata:        Metadata about the bear.
473
    """
474
    console_printer.print("{bear}:".format(bear=bear.__name__))
475
    console_printer.print("  " + metadata.desc + "\n")
476
477
    show_enumeration(console_printer,
478
                     "Used in:",
479
                     sections,
480
                     "  ",
481
                     "No sections.")
482
    show_enumeration(console_printer,
483
                     "Needed Settings:",
484
                     metadata.non_optional_params,
485
                     "  ",
486
                     "No needed settings.")
487
    show_enumeration(console_printer,
488
                     "Optional Settings:",
489
                     metadata.optional_params,
490
                     "  ",
491
                     "No optional settings.")
492
493
494
def print_bears(console_printer, bears, compress):
495
    """
496
    Presents all bears being used in a stylized manner.
497
498
    :param console_printer: Object to print messages on the console.
499
    :param bears:           Its a dictionary with bears as keys and list of
500
                            sections containing those bears as values.
501
    :param compress:            If set to true, output will be compressed (just
502
                                show bear names as a list)
503
    """
504
    if not bears:
505
        console_printer.print("No bears to show.")
506
    elif compress:
507
        bear_list = sorted(bears.keys(), key=lambda bear: bear.__name__)
508
        for bear in bear_list:
509
            console_printer.print(" *", bear.__name__)
510
    else:
511
        for bear in sorted(bears.keys(),
512
                           key=lambda bear: bear.__name__):
513
            show_bear(console_printer,
514
                      bear,
515
                      bears[bear],
516
                      bear.get_metadata())
517
518
519
def show_bears(local_bears, global_bears, compress, console_printer):
520
    """
521
    Extracts all the bears from each enabled section or the sections in the
522
    targets and passes a dictionary to the show_bears_callback method.
523
524
    :param local_bears:         Dictionary of local bears with section names
525
                                as keys and bear list as values.
526
    :param global_bears:        Dictionary of global bears with section
527
                                names as keys and bear list as values.
528
    :param compress:            If set to true, output will be compressed (just
529
                                show bear names as a list)
530
    :param show_bears_callback: The callback that is used to print these
531
                                bears. It will get one parameter holding
532
                                bears as key and the list of section names
533
                                where it's used as values.
534
    """
535
    bears = inverse_dicts(local_bears, global_bears)
536
537
    print_bears(console_printer, bears, compress)
538