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

warn_config_absent()   A

Complexity

Conditions 3

Size

Total Lines 4

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 4
rs 10
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
    if all(argument not in section for section in sections.values()):
111
        log_printer.warn("coala will not run any analysis. Did you forget "
112
                         "to give the `--{}` argument?".format(argument))
113
114
115
def load_configuration(arg_list, log_printer, arg_parser=None):
116
    """
117
    Parses the CLI args and loads the config file accordingly, taking
118
    default_coafile and the users .coarc into account.
119
120
    :param arg_list:    The list of command line arguments.
121
    :param log_printer: The LogPrinter object for logging.
122
    :return:            A tuple holding (log_printer: LogPrinter, sections:
123
                        dict(str, Section), targets: list(str)). (Types
124
                        indicated after colon.)
125
    """
126
    cli_sections = parse_cli(arg_list=arg_list, arg_parser=arg_parser)
127
    check_conflicts(cli_sections)
128
129
    if (
130
            bool(cli_sections["default"].get("find_config", "False")) and
131
            str(cli_sections["default"].get("config")) == ""):
132
        cli_sections["default"].add_or_create_setting(
133
            Setting("config", re.escape(find_user_config(os.getcwd()))))
134
135
    targets = []
136
    # We don't want to store targets argument back to file, thus remove it
137
    for item in list(cli_sections["default"].contents.pop("targets", "")):
138
        targets.append(item.lower())
139
140
    if bool(cli_sections["default"].get("no_config", "False")):
141
        sections = cli_sections
142
    else:
143
        default_sections = load_config_file(Constants.system_coafile,
144
                                            log_printer)
145
        user_sections = load_config_file(
146
            Constants.user_coafile,
147
            log_printer,
148
            silent=True)
149
150
        default_config = str(
151
            default_sections["default"].get("config", ".coafile"))
152
        user_config = str(user_sections["default"].get(
153
            "config", default_config))
154
        config = os.path.abspath(
155
            str(cli_sections["default"].get("config", user_config)))
156
157
        try:
158
            save = bool(cli_sections["default"].get("save", "False"))
159
        except ValueError:
160
            # A file is deposited for the save parameter, means we want to save
161
            # but to a specific file.
162
            save = True
163
164
        coafile_sections = load_config_file(config, log_printer, silent=save)
165
166
        sections = merge_section_dicts(default_sections, user_sections)
167
168
        sections = merge_section_dicts(sections, coafile_sections)
169
170
        sections = merge_section_dicts(sections, cli_sections)
171
172
    for section in sections:
173
        if section != "default":
174
            sections[section].defaults = sections["default"]
175
176
    str_log_level = str(sections["default"].get("log_level", "")).upper()
177
    log_printer.log_level = LOG_LEVEL.str_dict.get(str_log_level,
178
                                                   LOG_LEVEL.INFO)
179
180
    warn_config_absent(sections, 'files', log_printer)
181
    warn_config_absent(sections, 'bears', log_printer)
182
183
    return sections, targets
184
185
186
def find_user_config(file_path, max_trials=10):
187
    """
188
    Uses the filepath to find the most suitable user config file for the file
189
    by going down one directory at a time and finding config files there.
190
191
    :param file_path:  The path of the file whose user config needs to be found
192
    :param max_trials: The maximum number of directories to go down to.
193
    :return:           The config file's path, empty string if none was found
194
    """
195
    file_path = os.path.normpath(os.path.abspath(os.path.expanduser(
196
        file_path)))
197
    old_dir = None
198
    base_dir = (file_path if os.path.isdir(file_path)
199
                else os.path.dirname(file_path))
200
    home_dir = os.path.expanduser("~")
201
202
    while base_dir != old_dir and old_dir != home_dir and max_trials != 0:
203
        config_file = os.path.join(base_dir, ".coafile")
204
        if os.path.isfile(config_file):
205
            return config_file
206
207
        old_dir = base_dir
208
        base_dir = os.path.dirname(old_dir)
209
        max_trials = max_trials - 1
210
211
    return ""
212
213
214
def get_config_directory(section):
215
    """
216
    Retrieves the configuration directory for the given section.
217
218
    Given an empty section:
219
220
    >>> section = Section("name")
221
222
    The configuration directory is not defined and will therefore fallback to
223
    the current directory:
224
225
    >>> get_config_directory(section) == os.path.abspath(".")
226
    True
227
228
    If the ``files`` setting is given with an originating coafile, the directory
229
    of the coafile will be assumed the configuration directory:
230
231
    >>> section.append(Setting("files", "**", origin="/tmp/.coafile"))
232
    >>> get_config_directory(section) == os.path.abspath('/tmp/')
233
    True
234
235
    However if its origin is already a directory this will be preserved:
236
237
    >>> section['files'].origin = os.path.abspath('/tmp/dir/')
238
    >>> os.makedirs(section['files'].origin, exist_ok=True)
239
    >>> get_config_directory(section) == section['files'].origin
240
    True
241
242
    The user can manually set a project directory with the ``project_dir``
243
    setting:
244
245
    >>> section.append(Setting('project_dir', os.path.abspath('/tmp'), '/'))
246
    >>> get_config_directory(section) == os.path.abspath('/tmp')
247
    True
248
249
    If no section is given, the current directory is returned:
250
251
    >>> get_config_directory(None) == os.path.abspath(".")
252
    True
253
254
    To summarize, the config directory will be chosen by the following
255
    priorities if possible in that order:
256
257
    - the ``project_dir`` setting
258
    - the origin of the ``files`` setting, if it's a directory
259
    - the directory of the origin of the ``files`` setting
260
    - the current directory
261
262
    :param section: The section to inspect.
263
    :return: The directory where the project is lying.
264
    """
265
    if section is None:
266
        return os.getcwd()
267
268
    if 'project_dir' in section:
269
        return path(section.get('project_dir'))
270
271
    config = os.path.abspath(section.get('files', '').origin)
272
    return config if os.path.isdir(config) else os.path.dirname(config)
273
274
275
def gather_configuration(acquire_settings,
276
                         log_printer,
277
                         autoapply=None,
278
                         arg_list=None,
279
                         arg_parser=None):
280
    """
281
    Loads all configuration files, retrieves bears and all needed
282
    settings, saves back if needed and warns about non-existent targets.
283
284
    This function:
285
286
    -  Reads and merges all settings in sections from
287
288
       -  Default config
289
       -  User config
290
       -  Configuration file
291
       -  CLI
292
293
    -  Collects all the bears
294
    -  Fills up all needed settings
295
    -  Writes back the new sections to the configuration file if needed
296
    -  Gives all information back to caller
297
298
    :param acquire_settings: The method to use for requesting settings. It will
299
                             get a parameter which is a dictionary with the
300
                             settings name as key and a list containing a
301
                             description in [0] and the names of the bears
302
                             who need this setting in all following indexes.
303
    :param log_printer:      The log printer to use for logging. The log level
304
                             will be adjusted to the one given by the section.
305
    :param autoapply:        Set whether to autoapply patches. This is
306
                             overridable via any configuration file/CLI.
307
    :param arg_list:         CLI args to use
308
    :return:                 A tuple with the following contents:
309
310
                             -  A dictionary with the sections
311
                             -  Dictionary of list of local bears for each
312
                                section
313
                             -  Dictionary of list of global bears for each
314
                                section
315
                             -  The targets list
316
    """
317
    # Note: arg_list can also be []. Hence we cannot use
318
    # `arg_list = arg_list or default_list`
319
    arg_list = sys.argv[1:] if arg_list is None else arg_list
320
    sections, targets = load_configuration(arg_list, log_printer, arg_parser)
321
    local_bears, global_bears = fill_settings(sections,
322
                                              acquire_settings,
323
                                              log_printer)
324
    save_sections(sections)
325
    warn_nonexistent_targets(targets, sections, log_printer)
326
327
    if autoapply is not None:
328
        if not autoapply and 'autoapply' not in sections['default']:
329
            sections['default']['autoapply'] = "False"
330
331
    return (sections,
332
            local_bears,
333
            global_bears,
334
            targets)
335