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
« 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
5# pylint: disable=duplicate-code
7from __future__ import annotations
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
20from pylint import utils
21from pylint.config.option import Option
22from pylint.config.option_parser import OptionParser
23from pylint.typing import OptionDict
25if sys.version_info >= (3, 11):
26 import tomllib
27else:
28 import tomli as tomllib
31def _expand_default(self, option):
32 """Patch OptionParser.expand_default with custom behaviour.
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))
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
65class OptionsManagerMixIn:
66 """Handle configuration from both a configuration file and command line options."""
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
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)
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)
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)
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)
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
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)
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)
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
232 def load_provider_defaults(self):
233 """Initialize configuration using default values."""
234 for provider in self.options_providers:
235 provider.load_defaults()
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!")
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
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)
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)
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
317 def load_configuration(self, **kwargs):
318 """Override configuration according to given parameters."""
319 return self.load_configuration_from_config(kwargs)
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)
327 def load_command_line_configuration(self, args=None) -> list[str]:
328 """Override configuration according to command line parameters.
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
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()