Failed Conditions
Pull Request — master (#1626)
by Abdeali
01:44
created

coalib.output.acquire_actions_and_apply()   C

Complexity

Conditions 7

Size

Total Lines 47

Duplication

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