patman: fail early in Setup when provided config file does not exist
[platform/kernel/u-boot.git] / tools / patman / settings.py
1 # SPDX-License-Identifier: GPL-2.0+
2 # Copyright (c) 2011 The Chromium OS Authors.
3 #
4
5 try:
6     import configparser as ConfigParser
7 except Exception:
8     import ConfigParser
9
10 import argparse
11 import os
12 import re
13
14 from patman import gitutil
15
16 """Default settings per-project.
17
18 These are used by _ProjectConfigParser.  Settings names should match
19 the "dest" of the option parser from patman.py.
20 """
21 _default_settings = {
22     "u-boot": {},
23     "linux": {
24         "process_tags": "False",
25         "check_patch_use_tree": "True",
26     },
27     "gcc": {
28         "process_tags": "False",
29         "add_signoff": "False",
30         "check_patch": "False",
31     },
32 }
33
34
35 class _ProjectConfigParser(ConfigParser.ConfigParser):
36     """ConfigParser that handles projects.
37
38     There are two main goals of this class:
39     - Load project-specific default settings.
40     - Merge general default settings/aliases with project-specific ones.
41
42     # Sample config used for tests below...
43     >>> from io import StringIO
44     >>> sample_config = '''
45     ... [alias]
46     ... me: Peter P. <likesspiders@example.com>
47     ... enemies: Evil <evil@example.com>
48     ...
49     ... [sm_alias]
50     ... enemies: Green G. <ugly@example.com>
51     ...
52     ... [sm2_alias]
53     ... enemies: Doc O. <pus@example.com>
54     ...
55     ... [settings]
56     ... am_hero: True
57     ... '''
58
59     # Check to make sure that bogus project gets general alias.
60     >>> config = _ProjectConfigParser("zzz")
61     >>> config.readfp(StringIO(sample_config))
62     >>> str(config.get("alias", "enemies"))
63     'Evil <evil@example.com>'
64
65     # Check to make sure that alias gets overridden by project.
66     >>> config = _ProjectConfigParser("sm")
67     >>> config.readfp(StringIO(sample_config))
68     >>> str(config.get("alias", "enemies"))
69     'Green G. <ugly@example.com>'
70
71     # Check to make sure that settings get merged with project.
72     >>> config = _ProjectConfigParser("linux")
73     >>> config.readfp(StringIO(sample_config))
74     >>> sorted((str(a), str(b)) for (a, b) in config.items("settings"))
75     [('am_hero', 'True'), ('check_patch_use_tree', 'True'), ('process_tags', 'False')]
76
77     # Check to make sure that settings works with unknown project.
78     >>> config = _ProjectConfigParser("unknown")
79     >>> config.readfp(StringIO(sample_config))
80     >>> sorted((str(a), str(b)) for (a, b) in config.items("settings"))
81     [('am_hero', 'True')]
82     """
83     def __init__(self, project_name):
84         """Construct _ProjectConfigParser.
85
86         In addition to standard ConfigParser initialization, this also
87         loads project defaults.
88
89         Args:
90             project_name: The name of the project.
91         """
92         self._project_name = project_name
93         ConfigParser.ConfigParser.__init__(self)
94
95         # Update the project settings in the config based on
96         # the _default_settings global.
97         project_settings = "%s_settings" % project_name
98         if not self.has_section(project_settings):
99             self.add_section(project_settings)
100         project_defaults = _default_settings.get(project_name, {})
101         for setting_name, setting_value in project_defaults.items():
102             self.set(project_settings, setting_name, setting_value)
103
104     def get(self, section, option, *args, **kwargs):
105         """Extend ConfigParser to try project_section before section.
106
107         Args:
108             See ConfigParser.
109         Returns:
110             See ConfigParser.
111         """
112         try:
113             val = ConfigParser.ConfigParser.get(
114                 self, "%s_%s" % (self._project_name, section), option,
115                 *args, **kwargs
116             )
117         except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
118             val = ConfigParser.ConfigParser.get(
119                 self, section, option, *args, **kwargs
120             )
121         return val
122
123     def items(self, section, *args, **kwargs):
124         """Extend ConfigParser to add project_section to section.
125
126         Args:
127             See ConfigParser.
128         Returns:
129             See ConfigParser.
130         """
131         project_items = []
132         has_project_section = False
133         top_items = []
134
135         # Get items from the project section
136         try:
137             project_items = ConfigParser.ConfigParser.items(
138                 self, "%s_%s" % (self._project_name, section), *args, **kwargs
139             )
140             has_project_section = True
141         except ConfigParser.NoSectionError:
142             pass
143
144         # Get top-level items
145         try:
146             top_items = ConfigParser.ConfigParser.items(
147                 self, section, *args, **kwargs
148             )
149         except ConfigParser.NoSectionError:
150             # If neither section exists raise the error on...
151             if not has_project_section:
152                 raise
153
154         item_dict = dict(top_items)
155         item_dict.update(project_items)
156         return {(item, val) for item, val in item_dict.items()}
157
158
159 def ReadGitAliases(fname):
160     """Read a git alias file. This is in the form used by git:
161
162     alias uboot  u-boot@lists.denx.de
163     alias wd     Wolfgang Denk <wd@denx.de>
164
165     Args:
166         fname: Filename to read
167     """
168     try:
169         fd = open(fname, 'r', encoding='utf-8')
170     except IOError:
171         print("Warning: Cannot find alias file '%s'" % fname)
172         return
173
174     re_line = re.compile(r'alias\s+(\S+)\s+(.*)')
175     for line in fd.readlines():
176         line = line.strip()
177         if not line or line[0] == '#':
178             continue
179
180         m = re_line.match(line)
181         if not m:
182             print("Warning: Alias file line '%s' not understood" % line)
183             continue
184
185         list = alias.get(m.group(1), [])
186         for item in m.group(2).split(','):
187             item = item.strip()
188             if item:
189                 list.append(item)
190         alias[m.group(1)] = list
191
192     fd.close()
193
194
195 def CreatePatmanConfigFile(config_fname):
196     """Creates a config file under $(HOME)/.patman if it can't find one.
197
198     Args:
199         config_fname: Default config filename i.e., $(HOME)/.patman
200
201     Returns:
202         None
203     """
204     name = gitutil.get_default_user_name()
205     if name is None:
206         name = input("Enter name: ")
207
208     email = gitutil.get_default_user_email()
209
210     if email is None:
211         email = input("Enter email: ")
212
213     try:
214         f = open(config_fname, 'w')
215     except IOError:
216         print("Couldn't create patman config file\n")
217         raise
218
219     print('''[alias]
220 me: %s <%s>
221
222 [bounces]
223 nxp = Zhikang Zhang <zhikang.zhang@nxp.com>
224 ''' % (name, email), file=f)
225     f.close()
226
227
228 def _UpdateDefaults(main_parser, config):
229     """Update the given OptionParser defaults based on config.
230
231     We'll walk through all of the settings from all parsers.
232     For each setting we'll look for a default in the option parser.
233     If it's found we'll update the option parser default.
234
235     The idea here is that the .patman file should be able to update
236     defaults but that command line flags should still have the final
237     say.
238
239     Args:
240         parser: An instance of an ArgumentParser whose defaults will be
241             updated.
242         config: An instance of _ProjectConfigParser that we will query
243             for settings.
244     """
245     # Find all the parsers and subparsers
246     parsers = [main_parser]
247     parsers += [subparser for action in main_parser._actions
248                 if isinstance(action, argparse._SubParsersAction)
249                 for _, subparser in action.choices.items()]
250
251     # Collect the defaults from each parser
252     defaults = {}
253     parser_defaults = []
254     for parser in parsers:
255         pdefs = parser.parse_known_args()[0]
256         parser_defaults.append(pdefs)
257         defaults.update(vars(pdefs))
258
259     # Go through the settings and collect defaults
260     for name, val in config.items('settings'):
261         if name in defaults:
262             default_val = defaults[name]
263             if isinstance(default_val, bool):
264                 val = config.getboolean('settings', name)
265             elif isinstance(default_val, int):
266                 val = config.getint('settings', name)
267             elif isinstance(default_val, str):
268                 val = config.get('settings', name)
269             defaults[name] = val
270         else:
271             print("WARNING: Unknown setting %s" % name)
272
273     # Set all the defaults and manually propagate them to subparsers
274     main_parser.set_defaults(**defaults)
275     for parser, pdefs in zip(parsers, parser_defaults):
276         parser.set_defaults(**{k: v for k, v in defaults.items()
277                                if k in pdefs})
278
279
280 def _ReadAliasFile(fname):
281     """Read in the U-Boot git alias file if it exists.
282
283     Args:
284         fname: Filename to read.
285     """
286     if os.path.exists(fname):
287         bad_line = None
288         with open(fname, encoding='utf-8') as fd:
289             linenum = 0
290             for line in fd:
291                 linenum += 1
292                 line = line.strip()
293                 if not line or line.startswith('#'):
294                     continue
295                 words = line.split(None, 2)
296                 if len(words) < 3 or words[0] != 'alias':
297                     if not bad_line:
298                         bad_line = "%s:%d:Invalid line '%s'" % (fname, linenum,
299                                                                 line)
300                     continue
301                 alias[words[1]] = [s.strip() for s in words[2].split(',')]
302         if bad_line:
303             print(bad_line)
304
305
306 def _ReadBouncesFile(fname):
307     """Read in the bounces file if it exists
308
309     Args:
310         fname: Filename to read.
311     """
312     if os.path.exists(fname):
313         with open(fname) as fd:
314             for line in fd:
315                 if line.startswith('#'):
316                     continue
317                 bounces.add(line.strip())
318
319
320 def GetItems(config, section):
321     """Get the items from a section of the config.
322
323     Args:
324         config: _ProjectConfigParser object containing settings
325         section: name of section to retrieve
326
327     Returns:
328         List of (name, value) tuples for the section
329     """
330     try:
331         return config.items(section)
332     except ConfigParser.NoSectionError:
333         return []
334
335
336 def Setup(parser, project_name, config_fname=None):
337     """Set up the settings module by reading config files.
338
339     Args:
340         parser:         The parser to update.
341         project_name:   Name of project that we're working on; we'll look
342             for sections named "project_section" as well.
343         config_fname:   Config filename to read.  An error is raised if it
344             does not exist.
345     """
346     # First read the git alias file if available
347     _ReadAliasFile('doc/git-mailrc')
348     config = _ProjectConfigParser(project_name)
349
350     if config_fname and not os.path.exists(config_fname):
351         raise Exception(f'provided {config_fname} does not exist')
352
353     if not config_fname:
354         config_fname = '%s/.patman' % os.getenv('HOME')
355
356     if not os.path.exists(config_fname):
357         print("No config file found ~/.patman\nCreating one...\n")
358         CreatePatmanConfigFile(config_fname)
359
360     config.read(config_fname)
361
362     for name, value in GetItems(config, 'alias'):
363         alias[name] = value.split(',')
364
365     _ReadBouncesFile('doc/bounces')
366     for name, value in GetItems(config, 'bounces'):
367         bounces.add(value)
368
369     _UpdateDefaults(parser, config)
370
371
372 # These are the aliases we understand, indexed by alias. Each member is a list.
373 alias = {}
374 bounces = set()
375
376 if __name__ == "__main__":
377     import doctest
378
379     doctest.testmod()