|
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
|
|
|
|