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