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