1 # SPDX-License-Identifier: GPL-2.0+
2 # Copyright (c) 2011 The Chromium OS Authors.
3 # Copyright (c) 2022 Maxim Cournoyer <maxim.cournoyer@savoirfairelinux.com>
7 import configparser as ConfigParser
15 from patman import gitutil
17 """Default settings per-project.
19 These are used by _ProjectConfigParser. Settings names should match
20 the "dest" of the option parser from patman.py.
25 "process_tags": "False",
26 "check_patch_use_tree": "True",
29 "process_tags": "False",
30 "add_signoff": "False",
31 "check_patch": "False",
36 class _ProjectConfigParser(ConfigParser.ConfigParser):
37 """ConfigParser that handles projects.
39 There are two main goals of this class:
40 - Load project-specific default settings.
41 - Merge general default settings/aliases with project-specific ones.
43 # Sample config used for tests below...
44 >>> from io import StringIO
45 >>> sample_config = '''
47 ... me: Peter P. <likesspiders@example.com>
48 ... enemies: Evil <evil@example.com>
51 ... enemies: Green G. <ugly@example.com>
54 ... enemies: Doc O. <pus@example.com>
60 # Check to make sure that bogus project gets general alias.
61 >>> config = _ProjectConfigParser("zzz")
62 >>> config.readfp(StringIO(sample_config))
63 >>> str(config.get("alias", "enemies"))
64 'Evil <evil@example.com>'
66 # Check to make sure that alias gets overridden by project.
67 >>> config = _ProjectConfigParser("sm")
68 >>> config.readfp(StringIO(sample_config))
69 >>> str(config.get("alias", "enemies"))
70 'Green G. <ugly@example.com>'
72 # Check to make sure that settings get merged with project.
73 >>> config = _ProjectConfigParser("linux")
74 >>> config.readfp(StringIO(sample_config))
75 >>> sorted((str(a), str(b)) for (a, b) in config.items("settings"))
76 [('am_hero', 'True'), ('check_patch_use_tree', 'True'), ('process_tags', 'False')]
78 # Check to make sure that settings works with unknown project.
79 >>> config = _ProjectConfigParser("unknown")
80 >>> config.readfp(StringIO(sample_config))
81 >>> sorted((str(a), str(b)) for (a, b) in config.items("settings"))
84 def __init__(self, project_name):
85 """Construct _ProjectConfigParser.
87 In addition to standard ConfigParser initialization, this also
88 loads project defaults.
91 project_name: The name of the project.
93 self._project_name = project_name
94 ConfigParser.ConfigParser.__init__(self)
96 # Update the project settings in the config based on
97 # the _default_settings global.
98 project_settings = "%s_settings" % project_name
99 if not self.has_section(project_settings):
100 self.add_section(project_settings)
101 project_defaults = _default_settings.get(project_name, {})
102 for setting_name, setting_value in project_defaults.items():
103 self.set(project_settings, setting_name, setting_value)
105 def get(self, section, option, *args, **kwargs):
106 """Extend ConfigParser to try project_section before section.
114 val = ConfigParser.ConfigParser.get(
115 self, "%s_%s" % (self._project_name, section), option,
118 except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
119 val = ConfigParser.ConfigParser.get(
120 self, section, option, *args, **kwargs
124 def items(self, section, *args, **kwargs):
125 """Extend ConfigParser to add project_section to section.
133 has_project_section = False
136 # Get items from the project section
138 project_items = ConfigParser.ConfigParser.items(
139 self, "%s_%s" % (self._project_name, section), *args, **kwargs
141 has_project_section = True
142 except ConfigParser.NoSectionError:
145 # Get top-level items
147 top_items = ConfigParser.ConfigParser.items(
148 self, section, *args, **kwargs
150 except ConfigParser.NoSectionError:
151 # If neither section exists raise the error on...
152 if not has_project_section:
155 item_dict = dict(top_items)
156 item_dict.update(project_items)
157 return {(item, val) for item, val in item_dict.items()}
160 def ReadGitAliases(fname):
161 """Read a git alias file. This is in the form used by git:
163 alias uboot u-boot@lists.denx.de
164 alias wd Wolfgang Denk <wd@denx.de>
167 fname: Filename to read
170 fd = open(fname, 'r', encoding='utf-8')
172 print("Warning: Cannot find alias file '%s'" % fname)
175 re_line = re.compile(r'alias\s+(\S+)\s+(.*)')
176 for line in fd.readlines():
178 if not line or line[0] == '#':
181 m = re_line.match(line)
183 print("Warning: Alias file line '%s' not understood" % line)
186 list = alias.get(m.group(1), [])
187 for item in m.group(2).split(','):
191 alias[m.group(1)] = list
196 def CreatePatmanConfigFile(config_fname):
197 """Creates a config file under $(HOME)/.patman if it can't find one.
200 config_fname: Default config filename i.e., $(HOME)/.patman
205 name = gitutil.get_default_user_name()
207 name = input("Enter name: ")
209 email = gitutil.get_default_user_email()
212 email = input("Enter email: ")
215 f = open(config_fname, 'w')
217 print("Couldn't create patman config file\n")
224 nxp = Zhikang Zhang <zhikang.zhang@nxp.com>
225 ''' % (name, email), file=f)
229 def _UpdateDefaults(main_parser, config):
230 """Update the given OptionParser defaults based on config.
232 We'll walk through all of the settings from all parsers.
233 For each setting we'll look for a default in the option parser.
234 If it's found we'll update the option parser default.
236 The idea here is that the .patman file should be able to update
237 defaults but that command line flags should still have the final
241 parser: An instance of an ArgumentParser whose defaults will be
243 config: An instance of _ProjectConfigParser that we will query
246 # Find all the parsers and subparsers
247 parsers = [main_parser]
248 parsers += [subparser for action in main_parser._actions
249 if isinstance(action, argparse._SubParsersAction)
250 for _, subparser in action.choices.items()]
252 # Collect the defaults from each parser
255 for parser in parsers:
256 pdefs = parser.parse_known_args()[0]
257 parser_defaults.append(pdefs)
258 defaults.update(vars(pdefs))
260 # Go through the settings and collect defaults
261 for name, val in config.items('settings'):
263 default_val = defaults[name]
264 if isinstance(default_val, bool):
265 val = config.getboolean('settings', name)
266 elif isinstance(default_val, int):
267 val = config.getint('settings', name)
268 elif isinstance(default_val, str):
269 val = config.get('settings', name)
272 print("WARNING: Unknown setting %s" % name)
274 # Set all the defaults and manually propagate them to subparsers
275 main_parser.set_defaults(**defaults)
276 for parser, pdefs in zip(parsers, parser_defaults):
277 parser.set_defaults(**{k: v for k, v in defaults.items()
281 def _ReadAliasFile(fname):
282 """Read in the U-Boot git alias file if it exists.
285 fname: Filename to read.
287 if os.path.exists(fname):
289 with open(fname, encoding='utf-8') as fd:
294 if not line or line.startswith('#'):
296 words = line.split(None, 2)
297 if len(words) < 3 or words[0] != 'alias':
299 bad_line = "%s:%d:Invalid line '%s'" % (fname, linenum,
302 alias[words[1]] = [s.strip() for s in words[2].split(',')]
307 def _ReadBouncesFile(fname):
308 """Read in the bounces file if it exists
311 fname: Filename to read.
313 if os.path.exists(fname):
314 with open(fname) as fd:
316 if line.startswith('#'):
318 bounces.add(line.strip())
321 def GetItems(config, section):
322 """Get the items from a section of the config.
325 config: _ProjectConfigParser object containing settings
326 section: name of section to retrieve
329 List of (name, value) tuples for the section
332 return config.items(section)
333 except ConfigParser.NoSectionError:
337 def Setup(parser, project_name, config_fname=None):
338 """Set up the settings module by reading config files.
340 Unless `config_fname` is specified, a `.patman` config file local
341 to the git repository is consulted, followed by the global
342 `$HOME/.patman`. If none exists, the later is created. Values
343 defined in the local config file take precedence over those
344 defined in the global one.
347 parser: The parser to update.
348 project_name: Name of project that we're working on; we'll look
349 for sections named "project_section" as well.
350 config_fname: Config filename to read. An error is raised if it
353 # First read the git alias file if available
354 _ReadAliasFile('doc/git-mailrc')
355 config = _ProjectConfigParser(project_name)
357 if config_fname and not os.path.exists(config_fname):
358 raise Exception(f'provided {config_fname} does not exist')
361 config_fname = '%s/.patman' % os.getenv('HOME')
362 has_config = os.path.exists(config_fname)
364 git_local_config_fname = os.path.join(gitutil.get_top_level(), '.patman')
365 has_git_local_config = os.path.exists(git_local_config_fname)
367 # Read the git local config last, so that its values override
368 # those of the global config, if any.
370 config.read(config_fname)
371 if has_git_local_config:
372 config.read(git_local_config_fname)
374 if not (has_config or has_git_local_config):
375 print("No config file found.\nCreating ~/.patman...\n")
376 CreatePatmanConfigFile(config_fname)
378 for name, value in GetItems(config, 'alias'):
379 alias[name] = value.split(',')
381 _ReadBouncesFile('doc/bounces')
382 for name, value in GetItems(config, 'bounces'):
385 _UpdateDefaults(parser, config)
388 # These are the aliases we understand, indexed by alias. Each member is a list.
392 if __name__ == "__main__":