Coverage for C:\Repos\ekr-pylint\pylint\config\option_manager_mixin.py: 18%

217 statements  

« prev     ^ index     » next       coverage.py v6.4, created at 2022-05-24 10:21 -0500

1# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html 

2# For details: https://github.com/PyCQA/pylint/blob/main/LICENSE 

3# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt 

4 

5# pylint: disable=duplicate-code 

6 

7from __future__ import annotations 

8 

9import collections 

10import configparser 

11import contextlib 

12import copy 

13import optparse # pylint: disable=deprecated-module 

14import os 

15import sys 

16import warnings 

17from pathlib import Path 

18from typing import Any, TextIO 

19 

20from pylint import utils 

21from pylint.config.option import Option 

22from pylint.config.option_parser import OptionParser 

23from pylint.typing import OptionDict 

24 

25if sys.version_info >= (3, 11): 

26 import tomllib 

27else: 

28 import tomli as tomllib 

29 

30 

31def _expand_default(self, option): 

32 """Patch OptionParser.expand_default with custom behaviour. 

33 

34 This will handle defaults to avoid overriding values in the 

35 configuration file. 

36 """ 

37 if self.parser is None or not self.default_tag: 

38 return option.help 

39 optname = option._long_opts[0][2:] 

40 try: 

41 provider = self.parser.options_manager._all_options[optname] 

42 except KeyError: 

43 value = None 

44 else: 

45 optdict = provider.get_option_def(optname) 

46 optname = provider.option_attrname(optname, optdict) 

47 value = getattr(provider.config, optname, optdict) 

48 value = utils._format_option_value(optdict, value) 

49 if value is optparse.NO_DEFAULT or not value: 

50 value = self.NO_DEFAULT_VALUE 

51 return option.help.replace(self.default_tag, str(value)) 

52 

53 

54@contextlib.contextmanager 

55def _patch_optparse(): 

56 # pylint: disable = redefined-variable-type 

57 orig_default = optparse.HelpFormatter 

58 try: 

59 optparse.HelpFormatter.expand_default = _expand_default 

60 yield 

61 finally: 

62 optparse.HelpFormatter.expand_default = orig_default 

63 

64 

65class OptionsManagerMixIn: 

66 """Handle configuration from both a configuration file and command line options.""" 

67 

68 def __init__(self, usage): 

69 # TODO: 3.0: Remove deprecated class 

70 warnings.warn( 

71 "OptionsManagerMixIn has been deprecated and will be removed in pylint 3.0", 

72 DeprecationWarning, 

73 ) 

74 self.reset_parsers(usage) 

75 # list of registered options providers 

76 self.options_providers = [] 

77 # dictionary associating option name to checker 

78 self._all_options = collections.OrderedDict() 

79 self._short_options = {} 

80 self._nocallback_options = {} 

81 self._mygroups = {} 

82 # verbosity 

83 self._maxlevel = 0 

84 

85 def reset_parsers(self, usage=""): 

86 # configuration file parser 

87 self.cfgfile_parser = configparser.ConfigParser( 

88 inline_comment_prefixes=("#", ";") 

89 ) 

90 # command line parser 

91 self.cmdline_parser = OptionParser(Option, usage=usage) 

92 self.cmdline_parser.options_manager = self 

93 self._optik_option_attrs = set(self.cmdline_parser.option_class.ATTRS) 

94 

95 def register_options_provider(self, provider, own_group=True): 

96 """Register an options provider.""" 

97 self.options_providers.append(provider) 

98 non_group_spec_options = [ 

99 option for option in provider.options if "group" not in option[1] 

100 ] 

101 groups = getattr(provider, "option_groups", ()) 

102 if own_group and non_group_spec_options: 

103 self.add_option_group( 

104 provider.name.upper(), 

105 provider.__doc__, 

106 non_group_spec_options, 

107 provider, 

108 ) 

109 else: 

110 for opt, optdict in non_group_spec_options: 

111 self.add_optik_option(provider, self.cmdline_parser, opt, optdict) 

112 for gname, gdoc in groups: 

113 gname = gname.upper() 

114 goptions = [ 

115 option 

116 for option in provider.options 

117 if option[1].get("group", "").upper() == gname 

118 ] 

119 self.add_option_group(gname, gdoc, goptions, provider) 

120 

121 def add_option_group(self, group_name, _, options, provider): 

122 # add option group to the command line parser 

123 if group_name in self._mygroups: 

124 group = self._mygroups[group_name] 

125 else: 

126 group = optparse.OptionGroup( 

127 self.cmdline_parser, title=group_name.capitalize() 

128 ) 

129 self.cmdline_parser.add_option_group(group) 

130 self._mygroups[group_name] = group 

131 # add section to the config file 

132 if ( 

133 group_name != "DEFAULT" 

134 and group_name not in self.cfgfile_parser._sections 

135 ): 

136 self.cfgfile_parser.add_section(group_name) 

137 # add provider's specific options 

138 for opt, optdict in options: 

139 if not isinstance(optdict.get("action", "store"), str): 

140 optdict["action"] = "callback" 

141 self.add_optik_option(provider, group, opt, optdict) 

142 

143 def add_optik_option(self, provider, optikcontainer, opt, optdict): 

144 args, optdict = self.optik_option(provider, opt, optdict) 

145 option = optikcontainer.add_option(*args, **optdict) 

146 self._all_options[opt] = provider 

147 self._maxlevel = max(self._maxlevel, option.level or 0) 

148 

149 def optik_option(self, provider, opt, optdict): 

150 """Get our personal option definition and return a suitable form for 

151 use with optik/optparse. 

152 """ 

153 optdict = copy.copy(optdict) 

154 if "action" in optdict: 

155 self._nocallback_options[provider] = opt 

156 else: 

157 optdict["action"] = "callback" 

158 optdict["callback"] = self.cb_set_provider_option 

159 # default is handled here and *must not* be given to optik if you 

160 # want the whole machinery to work 

161 if "default" in optdict: 

162 if ( 

163 "help" in optdict 

164 and optdict.get("default") is not None 

165 and optdict["action"] not in ("store_true", "store_false") 

166 ): 

167 optdict["help"] += " [current: %default]" 

168 del optdict["default"] 

169 args = ["--" + str(opt)] 

170 if "short" in optdict: 

171 self._short_options[optdict["short"]] = opt 

172 args.append("-" + optdict["short"]) 

173 del optdict["short"] 

174 # cleanup option definition dict before giving it to optik 

175 for key in list(optdict.keys()): 

176 if key not in self._optik_option_attrs: 

177 optdict.pop(key) 

178 return args, optdict 

179 

180 def cb_set_provider_option(self, option, opt, value, parser): 

181 """Optik callback for option setting.""" 

182 if opt.startswith("--"): 

183 # remove -- on long option 

184 opt = opt[2:] 

185 else: 

186 # short option, get its long equivalent 

187 opt = self._short_options[opt[1:]] 

188 # trick since we can't set action='store_true' on options 

189 if value is None: 

190 value = 1 

191 self.global_set_option(opt, value) 

192 

193 def global_set_option(self, opt, value): 

194 """Set option on the correct option provider.""" 

195 self._all_options[opt].set_option(opt, value) 

196 

197 def generate_config( 

198 self, stream: TextIO | None = None, skipsections: tuple[str, ...] = () 

199 ) -> None: 

200 """Write a configuration file according to the current configuration 

201 into the given stream or stdout. 

202 """ 

203 options_by_section: dict[str, list[tuple[str, OptionDict, Any]]] = {} 

204 sections = [] 

205 for provider in self.options_providers: 

206 for section, options in provider.options_by_section(): 

207 if section is None: 

208 section = provider.name 

209 if section in skipsections: 

210 continue 

211 options = [ 

212 (n, d, v) 

213 for (n, d, v) in options 

214 if d.get("type") is not None and not d.get("deprecated") 

215 ] 

216 if not options: 

217 continue 

218 if section not in sections: 

219 sections.append(section) 

220 all_options = options_by_section.setdefault(section, []) 

221 all_options += options 

222 stream = stream or sys.stdout 

223 printed = False 

224 for section in sections: 

225 if printed: 

226 print("\n", file=stream) 

227 utils.format_section( 

228 stream, section.upper(), sorted(options_by_section[section]) 

229 ) 

230 printed = True 

231 

232 def load_provider_defaults(self): 

233 """Initialize configuration using default values.""" 

234 for provider in self.options_providers: 

235 provider.load_defaults() 

236 

237 def read_config_file( 

238 self, config_file: Path | None = None, verbose: bool = False 

239 ) -> None: 

240 """Read the configuration file but do not load it (i.e. dispatching 

241 values to each option's provider). 

242 """ 

243 if config_file: 

244 config_file = Path(os.path.expandvars(config_file)).expanduser() 

245 if not config_file.exists(): 

246 raise OSError(f"The config file {str(config_file)} doesn't exist!") 

247 

248 parser = self.cfgfile_parser 

249 if config_file.suffix == ".toml": 

250 try: 

251 self._parse_toml(config_file, parser) 

252 except tomllib.TOMLDecodeError: 

253 pass 

254 else: 

255 # Use this encoding in order to strip the BOM marker, if any. 

256 with open(config_file, encoding="utf_8_sig") as fp: 

257 parser.read_file(fp) 

258 # normalize each section's title 

259 for sect, values in list(parser._sections.items()): 

260 if sect.startswith("pylint."): 

261 sect = sect[len("pylint.") :] 

262 if not sect.isupper() and values: 

263 parser._sections[sect.upper()] = values 

264 

265 if not verbose: 

266 return 

267 if config_file and config_file.exists(): 

268 msg = f"Using config file '{config_file}'" 

269 else: 

270 msg = "No config file found, using default configuration" 

271 print(msg, file=sys.stderr) 

272 

273 def _parse_toml(self, config_file: Path, parser: configparser.ConfigParser) -> None: 

274 """Parse and handle errors of a toml configuration file.""" 

275 with open(config_file, mode="rb") as fp: 

276 content = tomllib.load(fp) 

277 try: 

278 sections_values = content["tool"]["pylint"] 

279 except KeyError: 

280 return 

281 for section, values in sections_values.items(): 

282 section_name = section.upper() 

283 # TOML has rich types, convert values to 

284 # strings as ConfigParser expects. 

285 if not isinstance(values, dict): 

286 # This class is a mixin: add_message comes from the `PyLinter` class 

287 self.add_message( # type: ignore[attr-defined] 

288 "bad-configuration-section", line=0, args=(section, values) 

289 ) 

290 continue 

291 for option, value in values.items(): 

292 if isinstance(value, bool): 

293 values[option] = "yes" if value else "no" 

294 elif isinstance(value, list): 

295 values[option] = ",".join(value) 

296 else: 

297 values[option] = str(value) 

298 for option, value in values.items(): 

299 try: 

300 parser.set(section_name, option, value=value) 

301 except configparser.NoSectionError: 

302 parser.add_section(section_name) 

303 parser.set(section_name, option, value=value) 

304 

305 def load_config_file(self): 

306 """Dispatch values previously read from a configuration file to each 

307 option's provider. 

308 """ 

309 parser = self.cfgfile_parser 

310 for section in parser.sections(): 

311 for option, value in parser.items(section): 

312 try: 

313 self.global_set_option(option, value) 

314 except (KeyError, optparse.OptionError): 

315 continue 

316 

317 def load_configuration(self, **kwargs): 

318 """Override configuration according to given parameters.""" 

319 return self.load_configuration_from_config(kwargs) 

320 

321 def load_configuration_from_config(self, config): 

322 for opt, opt_value in config.items(): 

323 opt = opt.replace("_", "-") 

324 provider = self._all_options[opt] 

325 provider.set_option(opt, opt_value) 

326 

327 def load_command_line_configuration(self, args=None) -> list[str]: 

328 """Override configuration according to command line parameters. 

329 

330 return additional arguments 

331 """ 

332 with _patch_optparse(): 

333 args = sys.argv[1:] if args is None else list(args) 

334 (options, args) = self.cmdline_parser.parse_args(args=args) 

335 for provider in self._nocallback_options: 

336 config = provider.config 

337 for attr in config.__dict__.keys(): 

338 value = getattr(options, attr, None) 

339 if value is None: 

340 continue 

341 setattr(config, attr, value) 

342 return args 

343 

344 def help(self, level=0): 

345 """Return the usage string for available options.""" 

346 self.cmdline_parser.formatter.output_level = level 

347 with _patch_optparse(): 

348 return self.cmdline_parser.format_help()