Completed
Pull Request — master (#1162)
by Mischa
01:44
created

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