Completed
Pull Request — master (#2277)
by Lasse
01:54
created

warn_config_absent()   A

Complexity

Conditions 3

Size

Total Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

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