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