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