Failed Conditions
Pull Request — master (#2076)
by Abdeali
02:04
created

coalib/output/ConsoleInteraction.py (1 issue)

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
import os.path
7
8
from pyprint.ConsolePrinter import ConsolePrinter
9
10
from coalib.misc.DictUtilities import inverse_dicts
11
from coalib.bearlib.spacing.SpacingHelper import SpacingHelper
12
from coalib.output.printers.LOG_LEVEL import LOG_LEVEL
13
from coalib.results.Result import Result
14
from coalib.results.result_actions.ApplyPatchAction import ApplyPatchAction
15
from coalib.results.result_actions.OpenEditorAction import OpenEditorAction
16
from coalib.results.result_actions.PrintDebugMessageAction import (
17
    PrintDebugMessageAction)
18
from coalib.results.result_actions.ShowPatchAction import ShowPatchAction
19
from coalib.results.RESULT_SEVERITY import (
20
    RESULT_SEVERITY, RESULT_SEVERITY_COLORS)
21
from coalib.settings.Setting import Setting
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 acquire_actions_and_apply(console_printer,
68
                              log_printer,
69
                              section,
70
                              file_diff_dict,
71
                              result,
72
                              file_dict,
73
                              cli_actions=None):
74
    """
75
    Acquires applicable actions and applies them.
76
77
    :param console_printer: Object to print messages on the console.
78
    :param log_printer:     Printer responsible for logging the messages.
79
    :param section:         Name of section to which the result belongs.
80
    :param file_diff_dict:  Dictionary containing filenames as keys and Diff
81
                            objects as values.
82
    :param result:          A derivative of Result.
83
    :param file_dict:       A dictionary containing all files with filename as
84
                            key.
85
    :param cli_actions:     The list of cli actions available.
86
    """
87
    cli_actions = cli_actions or CLI_ACTIONS
88
    failed_actions = set()
89
    while True:
90
        actions = []
91
        for action in cli_actions:
92
            if action.is_applicable(result, file_dict, file_diff_dict):
93
                actions.append(action)
94
95
        if actions == []:
96
            return
97
98
        action_dict = {}
99
        metadata_list = []
100
        for action in actions:
101
            metadata = action.get_metadata()
102
            action_dict[metadata.name] = action
103
            metadata_list.append(metadata)
104
105
        # User can always choose no action which is guaranteed to succeed
106
        if not ask_for_action_and_apply(log_printer,
107
                                        console_printer,
108
                                        section,
109
                                        metadata_list,
110
                                        action_dict,
111
                                        failed_actions,
112
                                        result,
113
                                        file_diff_dict,
114
                                        file_dict):
115
            break
116
117
118
def print_spaces_tabs_in_unicode(console_printer, line, tab_dict,
119
                                 color, index=0):
120
    """
121
    Prints the lines with tabs and spaces replaced by unicode symbols.
122
123
    :param console_printer: The ``Printer`` object to print to.
124
    :param line:            The line-text to print to ``console_printer``.
125
    :param tab_dict:        A dictionary containing the indices of tabs inside
126
                            ``line`` as keys and the tab-length as values.
127
    :param color:           The color to print the line with (except for spaces
128
                            and tabs.
129
    :param index:           The index from where to start the printing.
130
    """
131
    for char in line:
132
        if char == " ":
133
            try:
134
                console_printer.print("•", color='cyan', end='')
135
            except UnicodeEncodeError:
136
                console_printer.print(".", color='cyan', end='')
137
        elif char == '\t' and tab_dict:
138
            tab_count = tab_dict[index]
139
            console_printer.print(
140
                '-'*(tab_count-1) + '>', color='cyan', end='')
141
        else:
142
            console_printer.print(char, color=color, end='')
143
        index += 1
144
145
146
def print_lines(console_printer,
147
                file_dict,
148
                section,
149
                sourcerange):
150
    """
151
    Prints the lines between the current and the result line. If needed
152
    they will be shortened.
153
154
    :param console_printer: Object to print messages on the console.
155
    :param file_dict:       A dictionary containing all files as values with
156
                            filenames as key.
157
    :param sourcerange:     The SourceRange object referring to the related
158
                            lines to print.
159
    """
160
    for i in range(sourcerange.start.line, sourcerange.end.line + 1):
161
        console_printer.print(format_lines(lines='', line_nr=i),
162
                              color=FILE_LINES_COLOR,
163
                              end='')
164
        line = file_dict[sourcerange.file][i - 1].rstrip("\n")
165
        tab_width = int(section.get('tab_width', 4))
166
        s = SpacingHelper(tab_width)
167
        tab_dict = dict(s.yield_tab_lengths(line))
168
        printed_chars = 0
169
        if i == sourcerange.start.line and sourcerange.start.column:
170
            print_spaces_tabs_in_unicode(
171
                console_printer, line[:sourcerange.start.column-1],
172
                tab_dict, FILE_LINES_COLOR)
173
174
            printed_chars = sourcerange.start.column-1
175
176
        if i == sourcerange.end.line and sourcerange.end.column:
177
            print_spaces_tabs_in_unicode(
178
                console_printer, line[printed_chars:sourcerange.end.column-1],
179
                tab_dict, HIGHLIGHTED_CODE_COLOR, printed_chars)
180
181
            print_spaces_tabs_in_unicode(
182
                console_printer, line[sourcerange.end.column-1:],
183
                tab_dict, FILE_LINES_COLOR, sourcerange.end.column)
184
            console_printer.print("")
185
        else:
186
            print_spaces_tabs_in_unicode(
187
                console_printer, line[printed_chars:], tab_dict,
188
                HIGHLIGHTED_CODE_COLOR, printed_chars)
189
            console_printer.print("")
190
191
192
def print_result(console_printer,
193
                 log_printer,
194
                 section,
195
                 file_diff_dict,
196
                 result,
197
                 file_dict,
198
                 interactive=True):
199
    """
200
    Prints the result to console.
201
202
    :param console_printer: Object to print messages on the console.
203
    :param log_printer:     Printer responsible for logging the messages.
204
    :param section:         Name of section to which the result belongs.
205
    :param file_diff_dict:  Dictionary containing filenames as keys and Diff
206
                            objects as values.
207
    :param result:          A derivative of Result.
208
    :param file_dict:       A dictionary containing all files with filename as
209
                            key.
210
    :interactive:           Variable to check wether or not to
211
                            offer the user actions interactively.
212
    """
213
    if not isinstance(result, Result):
214
        log_printer.warn("One of the results can not be printed since it is "
215
                         "not a valid derivative of the coala result "
216
                         "class.")
217
        return
218
219
    console_printer.print(format_lines("[{sev}] {bear}:".format(
220
        sev=RESULT_SEVERITY.__str__(result.severity), bear=result.origin)),
221
        color=RESULT_SEVERITY_COLORS[result.severity])
222
    console_printer.print(format_lines(result.message), delimiter="\n")
223
224
    if interactive:
225
        acquire_actions_and_apply(console_printer,
226
                                  log_printer,
227
                                  section,
228
                                  file_diff_dict,
229
                                  result,
230
                                  file_dict)
231
232
233
def print_results_formatted(log_printer,
234
                            section,
235
                            result_list,
236
                            *args):
237
    format_str = str(section.get(
238
        "format_str",
239
        "id:{id}:origin:{origin}:file:{file}:line:{line}:column:"
240
        "{column}:end_line:{end_line}:end_column:{end_column}:severity:"
241
        "{severity}:severity_str:{severity_str}:message:{message}"))
242
    for result in result_list:
243
        severity_str = RESULT_SEVERITY.__str__(result.severity)
244
        try:
245
            if len(result.affected_code) == 0:
246
                print(format_str.format(file=None,
247
                                        line=None,
248
                                        end_line=None,
249
                                        column=None,
250
                                        end_column=None,
251
                                        severity_str=severity_str,
252
                                        **result.__dict__))
253
                continue
254
255
            for range in result.affected_code:
256
                print(format_str.format(file=range.start.file,
257
                                        line=range.start.line,
258
                                        end_line=range.end.line,
259
                                        column=range.start.column,
260
                                        end_column=range.end.column,
261
                                        severity_str=severity_str,
262
                                        **result.__dict__))
263
        except KeyError as exception:
264
            log_printer.log_exception(
265
                "Unable to print the result with the given format string.",
266
                exception)
267
268
269
def print_affected_files(console_printer,
270
                         log_printer,
271
                         section,
272
                         result,
273
                         file_dict,
274
                         color=True):
275
    """
276
    Print all the afected files and affected lines within them.
277
278
    :param console_printer: Object to print messages on the console.
279
    :param log_printer:     Printer responsible for logging the messages.
280
    :param section:         The section to which the results belong to.
281
    :param result_list:     List containing the results
282
    :param file_dict:       A dictionary containing all files with filename as
283
                            key.
284
    :param color:           Boolean variable to print the results in color or
285
                            not. Can be used for testing.
286
    """
287
    if len(result.affected_code) == 0:
288
        console_printer.print("\n" + STR_PROJECT_WIDE,
289
                              color=FILE_NAME_COLOR)
290
    else:
291
        for sourcerange in result.affected_code:
292
            if (
293
                    sourcerange.file is not None and
294
                    sourcerange.file not in file_dict):
295
                log_printer.warn("The context for the result ({}) cannot "
296
                                 "be printed because it refers to a file "
297
                                 "that doesn't seem to exist ({})"
298
                                 ".".format(str(result), sourcerange.file))
299
            else:
300
                print_affected_lines(console_printer,
301
                                     file_dict,
302
                                     section,
303
                                     sourcerange)
304
305
306
def print_results_no_input(log_printer,
307
                           section,
308
                           result_list,
309
                           file_dict,
310
                           file_diff_dict,
311
                           color=True):
312
    """
313
    Print all non interactive results in a section
314
315
    :param log_printer:    Printer responsible for logging the messages.
316
    :param section:        The section to which the results belong to.
317
    :param result_list:    List containing the results
318
    :param file_dict:      A dictionary containing all files with filename as
319
                           key.
320
    :param file_diff_dict: A dictionary that contains filenames as keys and
321
                           diff objects as values.
322
    :param color:          Boolean variable to print the results in color or
323
                           not. Can be used for testing.
324
    """
325
    console_printer = ConsolePrinter(print_colored=color)
326
    for result in result_list:
327
328
        print_affected_files(console_printer,
329
                             log_printer,
330
                             section,
331
                             result,
332
                             file_dict,
333
                             color=color)
334
335
        print_result(console_printer,
336
                     log_printer,
337
                     section,
338
                     file_diff_dict,
339
                     result,
340
                     file_dict,
341
                     interactive=False)
342
343
344
def print_results(log_printer,
345
                  section,
346
                  result_list,
347
                  file_dict,
348
                  file_diff_dict,
349
                  color=True):
350
    """
351
    Print all the results in a section.
352
353
    :param log_printer:    Printer responsible for logging the messages.
354
    :param section:        The section to which the results belong to.
355
    :param result_list:    List containing the results
356
    :param file_dict:      A dictionary containing all files with filename as
357
                           key.
358
    :param file_diff_dict: A dictionary that contains filenames as keys and
359
                           diff objects as values.
360
    :param color:          Boolean variable to print the results in color or
361
                           not. Can be used for testing.
362
    """
363
    console_printer = ConsolePrinter(print_colored=color)
364
365
    for result in sorted(result_list):
366
367
        print_affected_files(console_printer,
368
                             log_printer,
369
                             section,
370
                             result,
371
                             file_dict,
372
                             color=color)
373
374
        print_result(console_printer,
375
                     log_printer,
376
                     section,
377
                     file_diff_dict,
378
                     result,
379
                     file_dict)
380
381
382
def print_affected_lines(console_printer, file_dict, section, sourcerange):
383
    console_printer.print("\n" + os.path.relpath(sourcerange.file),
384
                          color=FILE_NAME_COLOR)
385
386
    if sourcerange.start.line is not None:
387
        if len(file_dict[sourcerange.file]) < sourcerange.end.line:
388
            console_printer.print(format_lines(lines=STR_LINE_DOESNT_EXIST))
389
        else:
390
            print_lines(console_printer,
391
                        file_dict,
392
                        section,
393
                        sourcerange)
394
395
396
def require_setting(log_printer, setting_name, arr):
397
    """
398
    This method is responsible for prompting a user about a missing setting and
399
    taking its value as input from the user.
400
401
    :param log_printer:  Printer responsible for logging the messages.
402
    :param setting_name: Name od the setting missing
403
    :param arr:          a list containing a description in [0] and the name
404
                         of the bears who need this setting in [1] and
405
                         following.
406
    """
407
    if not isinstance(arr, list) or len(arr) < 2:
408
        log_printer.log(LOG_LEVEL.WARNING,
409
                        "One of the given settings ({}) is not properly "
410
                        "described.".format(str(setting_name)))
411
412
        return None
413
414
    if len(arr) == 2:
415
        needed = arr[1]
416
    else:
417
        needed = ", ".join(arr[1:-1]) + " and " + arr[-1]
418
419
    return input(STR_GET_VAL_FOR_SETTING.format(str(setting_name),
420
                                                str(arr[0]),
421
                                                needed))
422
423
424
def acquire_settings(log_printer, settings_names_dict):
425
    """
426
    This method prompts the user for the given settings.
427
428
    :param log_printer: Printer responsible for logging the messages.
429
    :param settings:    a dictionary with the settings name as key and a list
430
                        containing a description in [0] and the name of the
431
                        bears who need this setting in [1] and following.
432
433
                        Example:
434
435
    ::
436
437
        {"UseTabs": ["describes whether tabs should be used instead of spaces",
438
                     "SpaceConsistencyBear",
439
                     "SomeOtherBear"]}
440
441
    :return:            a dictionary with the settings name as key and the
442
                        given value as value.
443
    """
444
    if not isinstance(settings_names_dict, dict):
445
        raise TypeError("The settings_names_dict parameter has to be a "
446
                        "dictionary.")
447
448
    result = {}
449
    for setting_name, arr in settings_names_dict.items():
450
        value = require_setting(log_printer, setting_name, arr)
451
        if value is not None:
452
            result[setting_name] = value
453
454
    return result
455
456
457
def get_action_info(section, action, failed_actions):
458
    """
459
    Get all the required Settings for an action. It updates the section with
460
    the Settings.
461
462
    :param section:         The section the action corresponds to.
463
    :param action:          The action to get the info for.
464
    :param failed_actions:  A set of all actions that have failed. A failed
465
                            action remains in the list until it is successfully
466
                            executed.
467
    :return:                Action name and the updated section.
468
    """
469
    params = action.non_optional_params
470
471
    if section is None:
472
        raise ValueError("section has to be intializied.")
473
474
    for param_name in params:
475
        if param_name not in section or action.name in failed_actions:
476
            question = format_lines(
477
                "Please enter a value for the parameter '{}' ({}): "
478
                .format(param_name, params[param_name][0]))
479
            section.append(Setting(param_name, input(question)))
480
481
    return action.name, section
482
483
484
def choose_action(console_printer, actions):
485
    """
486
    Presents the actions available to the user and takes as input the action
487
    the user wants to choose.
488
489
    :param console_printer: Object to print messages on the console.
490
    :param actions:         Actions available to the user.
491
    :return:                Return choice of action of user.
492
    """
493
    console_printer.print(format_lines(
494
        "The following actions are applicable to this result:"))
495
496
    while True:
497
        console_printer.print(format_lines(" 0: " +
498
                                           "Apply no further actions."))
499
        for i, action in enumerate(actions, 1):
500
            console_printer.print(format_lines("{:>2}: {}".format(
501
                i,
502
                action.desc)))
503
504
        try:
505
            line = format_lines("Please enter the number of the action "
506
                                "you want to execute. ")
507
            choice = int(input(line))
508
            if 0 <= choice <= len(actions):
509
                return choice
510
        except ValueError:
511
            pass
512
513
        console_printer.print(format_lines("Please enter a valid number."))
514
515
516
def print_actions(console_printer, section, actions, failed_actions):
517
    """
518
    Prints the given actions and lets the user choose.
519
520
    :param console_printer: Object to print messages on the console.
521
    :param actions:         A list of FunctionMetadata objects.
522
    :param failed_actions:  A set of all actions that have failed. A failed
523
                            action remains in the list until it is
524
                            successfully executed.
525
    :return:                A touple with the name member of the
526
                            FunctionMetadata object chosen by the user
527
                            and a Section containing at least all needed
528
                            values for the action. If the user did
529
                            choose to do nothing, return (None, None).
530
    """
531
    choice = choose_action(console_printer, actions)
532
533
    if choice == 0:
534
        return None, None
535
536
    return get_action_info(section, actions[choice - 1], failed_actions)
537
538
539
def ask_for_action_and_apply(log_printer,
540
                             console_printer,
541
                             section,
542
                             metadata_list,
543
                             action_dict,
544
                             failed_actions,
545
                             result,
546
                             file_diff_dict,
547
                             file_dict):
548
    """
549
    Asks the user for an action and applies it.
550
551
    :param log_printer:     Printer responsible for logging the messages.
552
    :param console_printer: Object to print messages on the console.
553
    :param section:         Currently active section.
554
    :param metadata_list:   Contains metadata for all the actions.
555
    :param action_dict:     Contains the action names as keys and their
556
                            references as values.
557
    :param failed_actions:  A set of all actions that have failed. A failed
558
                            action remains in the list until it is successfully
559
                            executed.
560
    :param result:          Result corresponding to the actions.
561
    :param file_diff_dict:  If its an action which applies a patch, this
562
                            contains the diff of the patch to be applied to
563
                            the file with filename as keys.
564
    :param file_dict:       Dictionary with filename as keys and its contents
565
                            as values.
566
    :return:                Returns a boolean value. True will be returned, if
567
                            it makes sense that the user may choose to execute
568
                            another action, False otherwise.
569
    """
570
    action_name, section = print_actions(console_printer, section,
571
                                         metadata_list, failed_actions)
572
    if action_name is None:
573
        return False
574
575
    chosen_action = action_dict[action_name]
576
    try:
577
        chosen_action.apply_from_section(result,
578
                                         file_dict,
579
                                         file_diff_dict,
580
                                         section)
581
        console_printer.print(
582
            format_lines(chosen_action.success_message),
583
            color=SUCCESS_COLOR)
584
        failed_actions.discard(action_name)
585
    except Exception as exception:  # pylint: disable=broad-except
586
        log_printer.log_exception("Failed to execute the action "
587
                                  "{} with error: {}.".format(action_name,
588
                                                              exception),
589
                                  exception)
590
        failed_actions.add(action_name)
591
    return True
592
593
594
def show_enumeration(console_printer,
595
                     title,
596
                     items,
597
                     indentation,
598
                     no_items_text):
599
    """
600
    This function takes as input an iterable object (preferably a list or
601
    a dict). And prints in a stylized format. If the iterable object is
602
    empty, it prints a specific statement give by the user. An e.g :
603
604
    <indentation>Title:
605
    <indentation> * Item 1
606
    <indentation> * Item 2
607
608
    :param console_printer: Object to print messages on the console.
609
    :param title:           Title of the text to be printed
610
    :param items:           The iterable object.
611
    :param indentation:     Number of spaces to indent every line by.
612
    :param no_items_text:   Text printed when iterable object is empty.
613
    """
614
    if not items:
615
        console_printer.print(indentation + no_items_text)
616
    else:
617
        console_printer.print(indentation + title)
618
        if isinstance(items, dict):
619
            for key, value in items.items():
620
                console_printer.print(indentation + " * " + key + ": " +
621
                                      value[0])
622
        else:
623
            for item in items:
624
                console_printer.print(indentation + " * " + item)
625
    console_printer.print()
626
627
628
def show_bear(console_printer, bear, sections, metadata):
629
    """
630
    Display all information about a bear.
631
632
    :param console_printer: Object to print messages on the console.
633
    :param bear:            The bear to be displayed.
634
    :param sections:        The sections to which the bear belongs.
635
    :param metadata:        Metadata about the bear.
636
    """
637
    console_printer.print("{bear}:".format(bear=bear.name))
638
    console_printer.print("  " + metadata.desc + "\n")
639
640
    show_enumeration(
641
        console_printer, "Supported languages:",
642
        (bear.LANGUAGES,) if isinstance(bear.LANGUAGES, str) else bear.LANGUAGES,
0 ignored issues
show
This line is too long as per the coding-style (81/80).

This check looks for lines that are too long. You can specify the maximum line length.

Loading history...
643
        "  ", "The bear does not provide information about which languages "
644
        "it can analyze.")
645
    show_enumeration(console_printer,
646
                     "Used in:",
647
                     sections,
648
                     "  ",
649
                     "No sections.")
650
651
    show_enumeration(console_printer,
652
                     "Needed Settings:",
653
                     metadata.non_optional_params,
654
                     "  ",
655
                     "No needed settings.")
656
    show_enumeration(console_printer,
657
                     "Optional Settings:",
658
                     metadata.optional_params,
659
                     "  ",
660
                     "No optional settings.")
661
662
663
def print_bears(console_printer, bears, compress):
664
    """
665
    Presents all bears being used in a stylized manner.
666
667
    :param console_printer: Object to print messages on the console.
668
    :param bears:           Its a dictionary with bears as keys and list of
669
                            sections containing those bears as values.
670
    :param compress:            If set to true, output will be compressed (just
671
                                show bear names as a list)
672
    """
673
    if not bears:
674
        console_printer.print("No bears to show.")
675
    elif compress:
676
        bear_list = sorted(bears.keys(), key=lambda bear: bear.name)
677
        for bear in bear_list:
678
            console_printer.print(" *", bear.name)
679
    else:
680
        for bear in sorted(bears.keys(),
681
                           key=lambda bear: bear.name):
682
            show_bear(console_printer,
683
                      bear,
684
                      bears[bear],
685
                      bear.get_metadata())
686
687
688
def show_bears(local_bears, global_bears, compress, console_printer):
689
    """
690
    Extracts all the bears from each enabled section or the sections in the
691
    targets and passes a dictionary to the show_bears_callback method.
692
693
    :param local_bears:         Dictionary of local bears with section names
694
                                as keys and bear list as values.
695
    :param global_bears:        Dictionary of global bears with section
696
                                names as keys and bear list as values.
697
    :param compress:            If set to true, output will be compressed (just
698
                                show bear names as a list)
699
    :param show_bears_callback: The callback that is used to print these
700
                                bears. It will get one parameter holding
701
                                bears as key and the list of section names
702
                                where it's used as values.
703
    """
704
    bears = inverse_dicts(local_bears, global_bears)
705
706
    print_bears(console_printer, bears, compress)
707