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