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