Completed
Pull Request — master (#2409)
by
unknown
02:05
created

configure_logging_from_json()   A

Complexity

Conditions 2

Size

Total Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
dl 0
loc 8
rs 9.4285
c 0
b 0
f 0
1
import os
2
import re
3
import sys
4
import json
5
import logging.config
6
7
from coalib.collecting.Collectors import (
8
    collect_all_bears_from_sections, filter_section_bears_by_languages)
9
from coalib.misc import Constants
10
from coalib.output.ConfWriter import ConfWriter
11
from coalib.output.printers.LOG_LEVEL import LOG_LEVEL
12
from coalib.parsing.CliParsing import parse_cli, check_conflicts
13
from coalib.parsing.ConfParser import ConfParser
14
from coalib.settings.Section import Section
15
from coalib.settings.SectionFilling import fill_settings
16
from coalib.settings.Setting import Setting, path
17
18
19
def merge_section_dicts(lower, higher):
20
    """
21
    Merges the section dictionaries. The values of higher will take
22
    precedence over the ones of lower. Lower will hold the modified dict in
23
    the end.
24
25
    :param lower:  A section.
26
    :param higher: A section which values will take precedence over the ones
27
                   from the other.
28
    :return:       The merged dict.
29
    """
30
    for name in higher:
31
        if name in lower:
32
            lower[name].update(higher[name], ignore_defaults=True)
33
        else:
34
            # no deep copy needed
35
            lower[name] = higher[name]
36
37
    return lower
38
39
40
def load_config_file(filename, log_printer, silent=False):
41
    """
42
    Loads sections from a config file. Prints an appropriate warning if
43
    it doesn't exist and returns a section dict containing an empty
44
    default section in that case.
45
46
    It assumes that the cli_sections are available.
47
48
    :param filename:    The file to load settings from.
49
    :param log_printer: The log printer to log the warning/error to (in case).
50
    :param silent:      Whether or not to warn the user/exit if the file
51
                        doesn't exist.
52
    :raises SystemExit: Exits when given filename is invalid and is not the
53
                        default coafile. Only raised when ``silent`` is
54
                        ``False``.
55
    """
56
    filename = os.path.abspath(filename)
57
58
    try:
59
        return ConfParser().parse(filename)
60
    except FileNotFoundError:
61
        if not silent:
62
            if os.path.basename(filename) == Constants.default_coafile:
63
                log_printer.warn("The default coafile {0!r} was not found. "
64
                                 "You can generate a configuration file with "
65
                                 "your current options by adding the `--save` "
66
                                 "flag.".format(Constants.default_coafile))
67
            else:
68
                log_printer.err("The requested coafile {0!r} does not exist. "
69
                                "You can generate it with your current "
70
                                "options by adding the `--save` flag."
71
                                .format(filename))
72
                sys.exit(2)
73
74
        return {"default": Section("default")}
75
76
77
def save_sections(sections):
78
    """
79
    Saves the given sections if they are to be saved.
80
81
    :param sections: A section dict.
82
    """
83
    default_section = sections["default"]
84
    try:
85
        if bool(default_section.get("save", "false")):
86
            conf_writer = ConfWriter(
87
                str(default_section.get("config", Constants.default_coafile)))
88
        else:
89
            return
90
    except ValueError:
91
        conf_writer = ConfWriter(str(default_section.get("save", ".coafile")))
92
93
    conf_writer.write_sections(sections)
94
    conf_writer.close()
95
96
97
def warn_nonexistent_targets(targets, sections, log_printer):
98
    """
99
    Prints out a warning on the given log printer for all targets that are
100
    not existent within the given sections.
101
102
    :param targets:     The targets to check.
103
    :param sections:    The sections to search. (Dict.)
104
    :param log_printer: The log printer to warn to.
105
    """
106
    for target in targets:
107
        if target not in sections:
108
            log_printer.warn(
109
                "The requested section '{section}' is not existent. "
110
                "Thus it cannot be executed.".format(section=target))
111
112
113
def warn_config_absent(sections, argument, log_printer):
114
    """
115
    Checks if the given argument is present somewhere in the sections and emits
116
    a warning that code analysis can not be run without it.
117
118
    :param sections:    A dictionary of sections.
119
    :param argument:    The argument to check for, e.g. "files".
120
    :param log_printer: A log printer to emit the warning to.
121
    """
122
    if all(argument not in section for section in sections.values()):
123
        log_printer.warn("coala will not run any analysis. Did you forget "
124
                         "to give the `--{}` argument?".format(argument))
125
126
127
def configure_logging_from_json(filename):
128
    """
129
    Configures logging module based on parameters from JSON file.
130
131
    :param filename:    Name of a file with logging configuration.
132
    """
133
    with open(filename) as cnf_file:
134
        logging.config.dictConfig(json.load(cnf_file))
135
136
137
def load_configuration(arg_list, log_printer, arg_parser=None):
138
    """
139
    Parses the CLI args and loads the config file accordingly, taking
140
    default_coafile and the users .coarc into account.
141
142
    :param arg_list:    The list of command line arguments.
143
    :param log_printer: The LogPrinter object for logging.
144
    :return:            A tuple holding (log_printer: LogPrinter, sections:
145
                        dict(str, Section), targets: list(str)). (Types
146
                        indicated after colon.)
147
    """
148
    cli_sections = parse_cli(arg_list=arg_list, arg_parser=arg_parser)
149
    check_conflicts(cli_sections)
150
151
    if (
152
            bool(cli_sections["default"].get("find_config", "False")) and
153
            str(cli_sections["default"].get("config")) == ""):
154
        cli_sections["default"].add_or_create_setting(
155
            Setting("config", re.escape(find_user_config(os.getcwd()))))
156
157
    targets = []
158
    # We don't want to store targets argument back to file, thus remove it
159
    for item in list(cli_sections["default"].contents.pop("targets", "")):
160
        targets.append(item.lower())
161
162
    if bool(cli_sections["default"].get("no_config", "False")):
163
        sections = cli_sections
164
    else:
165
        default_sections = load_config_file(Constants.system_coafile,
166
                                            log_printer)
167
        user_sections = load_config_file(
168
            Constants.user_coafile,
169
            log_printer,
170
            silent=True)
171
172
        default_config = str(
173
            default_sections["default"].get("config", ".coafile"))
174
        user_config = str(user_sections["default"].get(
175
            "config", default_config))
176
        config = os.path.abspath(
177
            str(cli_sections["default"].get("config", user_config)))
178
179
        try:
180
            save = bool(cli_sections["default"].get("save", "False"))
181
        except ValueError:
182
            # A file is deposited for the save parameter, means we want to save
183
            # but to a specific file.
184
            save = True
185
186
        coafile_sections = load_config_file(config, log_printer, silent=save)
187
188
        sections = merge_section_dicts(default_sections, user_sections)
189
190
        sections = merge_section_dicts(sections, coafile_sections)
191
192
        sections = merge_section_dicts(sections, cli_sections)
193
194
    for section in sections:
195
        if section != "default":
196
            sections[section].defaults = sections["default"]
197
198
    str_log_level = str(sections["default"].get("log_level", "")).upper()
199
    log_printer.log_level = LOG_LEVEL.str_dict.get(str_log_level,
200
                                                   LOG_LEVEL.INFO)
201
202
    warn_config_absent(sections, 'files', log_printer)
203
    warn_config_absent(sections, 'bears', log_printer)
204
205
    return sections, targets
206
207
208
def find_user_config(file_path, max_trials=10):
209
    """
210
    Uses the filepath to find the most suitable user config file for the file
211
    by going down one directory at a time and finding config files there.
212
213
    :param file_path:  The path of the file whose user config needs to be found
214
    :param max_trials: The maximum number of directories to go down to.
215
    :return:           The config file's path, empty string if none was found
216
    """
217
    file_path = os.path.normpath(os.path.abspath(os.path.expanduser(
218
        file_path)))
219
    old_dir = None
220
    base_dir = (file_path if os.path.isdir(file_path)
221
                else os.path.dirname(file_path))
222
    home_dir = os.path.expanduser("~")
223
224
    while base_dir != old_dir and old_dir != home_dir and max_trials != 0:
225
        config_file = os.path.join(base_dir, ".coafile")
226
        if os.path.isfile(config_file):
227
            return config_file
228
229
        old_dir = base_dir
230
        base_dir = os.path.dirname(old_dir)
231
        max_trials = max_trials - 1
232
233
    return ""
234
235
236
def get_config_directory(section):
237
    """
238
    Retrieves the configuration directory for the given section.
239
240
    Given an empty section:
241
242
    >>> section = Section("name")
243
244
    The configuration directory is not defined and will therefore fallback to
245
    the current directory:
246
247
    >>> get_config_directory(section) == os.path.abspath(".")
248
    True
249
250
    If the ``files`` setting is given with an originating coafile, the directory
251
    of the coafile will be assumed the configuration directory:
252
253
    >>> section.append(Setting("files", "**", origin="/tmp/.coafile"))
254
    >>> get_config_directory(section) == os.path.abspath('/tmp/')
255
    True
256
257
    However if its origin is already a directory this will be preserved:
258
259
    >>> section['files'].origin = os.path.abspath('/tmp/dir/')
260
    >>> os.makedirs(section['files'].origin, exist_ok=True)
261
    >>> get_config_directory(section) == section['files'].origin
262
    True
263
264
    The user can manually set a project directory with the ``project_dir``
265
    setting:
266
267
    >>> section.append(Setting('project_dir', os.path.abspath('/tmp'), '/'))
268
    >>> get_config_directory(section) == os.path.abspath('/tmp')
269
    True
270
271
    If no section is given, the current directory is returned:
272
273
    >>> get_config_directory(None) == os.path.abspath(".")
274
    True
275
276
    To summarize, the config directory will be chosen by the following
277
    priorities if possible in that order:
278
279
    - the ``project_dir`` setting
280
    - the origin of the ``files`` setting, if it's a directory
281
    - the directory of the origin of the ``files`` setting
282
    - the current directory
283
284
    :param section: The section to inspect.
285
    :return: The directory where the project is lying.
286
    """
287
    if section is None:
288
        return os.getcwd()
289
290
    if 'project_dir' in section:
291
        return path(section.get('project_dir'))
292
293
    config = os.path.abspath(section.get('files', '').origin)
294
    return config if os.path.isdir(config) else os.path.dirname(config)
295
296
297
def get_filtered_bears(languages, log_printer):
298
    """
299
    Fetch bears and filter them based on given list of languages.
300
301
    :param languages:   List of languages.
302
    :param log_printer: The log_printer to handle logging.
303
    :return:            Tuple containing dictionaries of local bears
304
                        and global bears.
305
    """
306
    sections, _ = load_configuration(arg_list=None,
307
                                     log_printer=log_printer)
308
    local_bears, global_bears = collect_all_bears_from_sections(
309
        sections, log_printer)
310
    if languages:
311
        local_bears = filter_section_bears_by_languages(
312
            local_bears, languages)
313
        global_bears = filter_section_bears_by_languages(
314
            global_bears, languages)
315
    return local_bears, global_bears
316
317
318
def gather_configuration(acquire_settings,
319
                         log_printer,
320
                         autoapply=None,
321
                         arg_list=None,
322
                         arg_parser=None):
323
    """
324
    Loads all configuration files, retrieves bears and all needed
325
    settings, saves back if needed and warns about non-existent targets.
326
327
    This function:
328
329
    -  Reads and merges all settings in sections from
330
331
       -  Default config
332
       -  User config
333
       -  Configuration file
334
       -  CLI
335
336
    -  Collects all the bears
337
    -  Fills up all needed settings
338
    -  Writes back the new sections to the configuration file if needed
339
    -  Gives all information back to caller
340
341
    :param acquire_settings: The method to use for requesting settings. It will
342
                             get a parameter which is a dictionary with the
343
                             settings name as key and a list containing a
344
                             description in [0] and the names of the bears
345
                             who need this setting in all following indexes.
346
    :param log_printer:      The log printer to use for logging. The log level
347
                             will be adjusted to the one given by the section.
348
    :param autoapply:        Set whether to autoapply patches. This is
349
                             overridable via any configuration file/CLI.
350
    :param arg_list:         CLI args to use
351
    :return:                 A tuple with the following contents:
352
353
                             -  A dictionary with the sections
354
                             -  Dictionary of list of local bears for each
355
                                section
356
                             -  Dictionary of list of global bears for each
357
                                section
358
                             -  The targets list
359
    """
360
    # Note: arg_list can also be []. Hence we cannot use
361
    # `arg_list = arg_list or default_list`
362
    arg_list = sys.argv[1:] if arg_list is None else arg_list
363
    sections, targets = load_configuration(arg_list, log_printer, arg_parser)
364
    local_bears, global_bears = fill_settings(sections,
365
                                              acquire_settings,
366
                                              log_printer)
367
    save_sections(sections)
368
    warn_nonexistent_targets(targets, sections, log_printer)
369
370
    if autoapply is not None:
371
        if not autoapply and 'autoapply' not in sections['default']:
372
            sections['default']['autoapply'] = "False"
373
374
    return (sections,
375
            local_bears,
376
            global_bears,
377
            targets)
378