]>
Commit | Line | Data |
---|---|---|
53e6db90 DC |
1 | """Option handling and Option management logic.""" |
2 | from __future__ import annotations | |
3 | ||
4 | import argparse | |
5 | import enum | |
6 | import functools | |
7 | import logging | |
8 | from typing import Any | |
9 | from typing import Callable | |
10 | from typing import Sequence | |
11 | ||
12 | from flake8 import utils | |
13 | from flake8.plugins.finder import Plugins | |
14 | ||
15 | LOG = logging.getLogger(__name__) | |
16 | ||
17 | # represent a singleton of "not passed arguments". | |
18 | # an enum is chosen to trick mypy | |
19 | _ARG = enum.Enum("_ARG", "NO") | |
20 | ||
21 | ||
22 | def _flake8_normalize( | |
23 | value: str, | |
24 | *args: str, | |
25 | comma_separated_list: bool = False, | |
26 | normalize_paths: bool = False, | |
27 | ) -> str | list[str]: | |
28 | ret: str | list[str] = value | |
29 | if comma_separated_list and isinstance(ret, str): | |
30 | ret = utils.parse_comma_separated_list(value) | |
31 | ||
32 | if normalize_paths: | |
33 | if isinstance(ret, str): | |
34 | ret = utils.normalize_path(ret, *args) | |
35 | else: | |
36 | ret = utils.normalize_paths(ret, *args) | |
37 | ||
38 | return ret | |
39 | ||
40 | ||
41 | class Option: | |
42 | """Our wrapper around an argparse argument parsers to add features.""" | |
43 | ||
44 | def __init__( | |
45 | self, | |
46 | short_option_name: str | _ARG = _ARG.NO, | |
47 | long_option_name: str | _ARG = _ARG.NO, | |
48 | # Options below are taken from argparse.ArgumentParser.add_argument | |
49 | action: str | type[argparse.Action] | _ARG = _ARG.NO, | |
50 | default: Any | _ARG = _ARG.NO, | |
51 | type: Callable[..., Any] | _ARG = _ARG.NO, | |
52 | dest: str | _ARG = _ARG.NO, | |
53 | nargs: int | str | _ARG = _ARG.NO, | |
54 | const: Any | _ARG = _ARG.NO, | |
55 | choices: Sequence[Any] | _ARG = _ARG.NO, | |
56 | help: str | _ARG = _ARG.NO, | |
57 | metavar: str | _ARG = _ARG.NO, | |
58 | required: bool | _ARG = _ARG.NO, | |
59 | # Options below here are specific to Flake8 | |
60 | parse_from_config: bool = False, | |
61 | comma_separated_list: bool = False, | |
62 | normalize_paths: bool = False, | |
63 | ) -> None: | |
64 | """Initialize an Option instance. | |
65 | ||
66 | The following are all passed directly through to argparse. | |
67 | ||
68 | :param short_option_name: | |
69 | The short name of the option (e.g., ``-x``). This will be the | |
70 | first argument passed to ``ArgumentParser.add_argument`` | |
71 | :param long_option_name: | |
72 | The long name of the option (e.g., ``--xtra-long-option``). This | |
73 | will be the second argument passed to | |
74 | ``ArgumentParser.add_argument`` | |
75 | :param default: | |
76 | Default value of the option. | |
77 | :param dest: | |
78 | Attribute name to store parsed option value as. | |
79 | :param nargs: | |
80 | Number of arguments to parse for this option. | |
81 | :param const: | |
82 | Constant value to store on a common destination. Usually used in | |
83 | conjunction with ``action="store_const"``. | |
84 | :param choices: | |
85 | Possible values for the option. | |
86 | :param help: | |
87 | Help text displayed in the usage information. | |
88 | :param metavar: | |
89 | Name to use instead of the long option name for help text. | |
90 | :param required: | |
91 | Whether this option is required or not. | |
92 | ||
93 | The following options may be passed directly through to :mod:`argparse` | |
94 | but may need some massaging. | |
95 | ||
96 | :param type: | |
97 | A callable to normalize the type (as is the case in | |
98 | :mod:`argparse`). | |
99 | :param action: | |
100 | Any action allowed by :mod:`argparse`. | |
101 | ||
102 | The following parameters are for Flake8's option handling alone. | |
103 | ||
104 | :param parse_from_config: | |
105 | Whether or not this option should be parsed out of config files. | |
106 | :param comma_separated_list: | |
107 | Whether the option is a comma separated list when parsing from a | |
108 | config file. | |
109 | :param normalize_paths: | |
110 | Whether the option is expecting a path or list of paths and should | |
111 | attempt to normalize the paths to absolute paths. | |
112 | """ | |
113 | if ( | |
114 | long_option_name is _ARG.NO | |
115 | and short_option_name is not _ARG.NO | |
116 | and short_option_name.startswith("--") | |
117 | ): | |
118 | short_option_name, long_option_name = _ARG.NO, short_option_name | |
119 | ||
120 | # flake8 special type normalization | |
121 | if comma_separated_list or normalize_paths: | |
122 | type = functools.partial( | |
123 | _flake8_normalize, | |
124 | comma_separated_list=comma_separated_list, | |
125 | normalize_paths=normalize_paths, | |
126 | ) | |
127 | ||
128 | self.short_option_name = short_option_name | |
129 | self.long_option_name = long_option_name | |
130 | self.option_args = [ | |
131 | x | |
132 | for x in (short_option_name, long_option_name) | |
133 | if x is not _ARG.NO | |
134 | ] | |
135 | self.action = action | |
136 | self.default = default | |
137 | self.type = type | |
138 | self.dest = dest | |
139 | self.nargs = nargs | |
140 | self.const = const | |
141 | self.choices = choices | |
142 | self.help = help | |
143 | self.metavar = metavar | |
144 | self.required = required | |
145 | self.option_kwargs: dict[str, Any | _ARG] = { | |
146 | "action": self.action, | |
147 | "default": self.default, | |
148 | "type": self.type, | |
149 | "dest": self.dest, | |
150 | "nargs": self.nargs, | |
151 | "const": self.const, | |
152 | "choices": self.choices, | |
153 | "help": self.help, | |
154 | "metavar": self.metavar, | |
155 | "required": self.required, | |
156 | } | |
157 | ||
158 | # Set our custom attributes | |
159 | self.parse_from_config = parse_from_config | |
160 | self.comma_separated_list = comma_separated_list | |
161 | self.normalize_paths = normalize_paths | |
162 | ||
163 | self.config_name: str | None = None | |
164 | if parse_from_config: | |
165 | if long_option_name is _ARG.NO: | |
166 | raise ValueError( | |
167 | "When specifying parse_from_config=True, " | |
168 | "a long_option_name must also be specified." | |
169 | ) | |
170 | self.config_name = long_option_name[2:].replace("-", "_") | |
171 | ||
172 | self._opt = None | |
173 | ||
174 | @property | |
175 | def filtered_option_kwargs(self) -> dict[str, Any]: | |
176 | """Return any actually-specified arguments.""" | |
177 | return { | |
178 | k: v for k, v in self.option_kwargs.items() if v is not _ARG.NO | |
179 | } | |
180 | ||
181 | def __repr__(self) -> str: # noqa: D105 | |
182 | parts = [] | |
183 | for arg in self.option_args: | |
184 | parts.append(arg) | |
185 | for k, v in self.filtered_option_kwargs.items(): | |
186 | parts.append(f"{k}={v!r}") | |
187 | return f"Option({', '.join(parts)})" | |
188 | ||
189 | def normalize(self, value: Any, *normalize_args: str) -> Any: | |
190 | """Normalize the value based on the option configuration.""" | |
191 | if self.comma_separated_list and isinstance(value, str): | |
192 | value = utils.parse_comma_separated_list(value) | |
193 | ||
194 | if self.normalize_paths: | |
195 | if isinstance(value, list): | |
196 | value = utils.normalize_paths(value, *normalize_args) | |
197 | else: | |
198 | value = utils.normalize_path(value, *normalize_args) | |
199 | ||
200 | return value | |
201 | ||
202 | def to_argparse(self) -> tuple[list[str], dict[str, Any]]: | |
203 | """Convert a Flake8 Option to argparse ``add_argument`` arguments.""" | |
204 | return self.option_args, self.filtered_option_kwargs | |
205 | ||
206 | ||
207 | class OptionManager: | |
208 | """Manage Options and OptionParser while adding post-processing.""" | |
209 | ||
210 | def __init__( | |
211 | self, | |
212 | *, | |
213 | version: str, | |
214 | plugin_versions: str, | |
215 | parents: list[argparse.ArgumentParser], | |
216 | formatter_names: list[str], | |
217 | ) -> None: | |
218 | """Initialize an instance of an OptionManager.""" | |
219 | self.formatter_names = formatter_names | |
220 | self.parser = argparse.ArgumentParser( | |
221 | prog="flake8", | |
222 | usage="%(prog)s [options] file file ...", | |
223 | parents=parents, | |
224 | epilog=f"Installed plugins: {plugin_versions}", | |
225 | ) | |
226 | self.parser.add_argument( | |
227 | "--version", | |
228 | action="version", | |
229 | version=( | |
230 | f"{version} ({plugin_versions}) " | |
231 | f"{utils.get_python_version()}" | |
232 | ), | |
233 | ) | |
234 | self.parser.add_argument("filenames", nargs="*", metavar="filename") | |
235 | ||
236 | self.config_options_dict: dict[str, Option] = {} | |
237 | self.options: list[Option] = [] | |
238 | self.extended_default_ignore: list[str] = [] | |
239 | self.extended_default_select: list[str] = [] | |
240 | ||
241 | self._current_group: argparse._ArgumentGroup | None = None | |
242 | ||
243 | # TODO: maybe make this a free function to reduce api surface area | |
244 | def register_plugins(self, plugins: Plugins) -> None: | |
245 | """Register the plugin options (if needed).""" | |
246 | groups: dict[str, argparse._ArgumentGroup] = {} | |
247 | ||
248 | def _set_group(name: str) -> None: | |
249 | try: | |
250 | self._current_group = groups[name] | |
251 | except KeyError: | |
252 | group = self.parser.add_argument_group(name) | |
253 | self._current_group = groups[name] = group | |
254 | ||
255 | for loaded in plugins.all_plugins(): | |
256 | add_options = getattr(loaded.obj, "add_options", None) | |
257 | if add_options: | |
258 | _set_group(loaded.plugin.package) | |
259 | add_options(self) | |
260 | ||
261 | if loaded.plugin.entry_point.group == "flake8.extension": | |
262 | self.extend_default_select([loaded.entry_name]) | |
263 | ||
264 | # isn't strictly necessary, but seems cleaner | |
265 | self._current_group = None | |
266 | ||
267 | def add_option(self, *args: Any, **kwargs: Any) -> None: | |
268 | """Create and register a new option. | |
269 | ||
270 | See parameters for :class:`~flake8.options.manager.Option` for | |
271 | acceptable arguments to this method. | |
272 | ||
273 | .. note:: | |
274 | ||
275 | ``short_option_name`` and ``long_option_name`` may be specified | |
276 | positionally as they are with argparse normally. | |
277 | """ | |
278 | option = Option(*args, **kwargs) | |
279 | option_args, option_kwargs = option.to_argparse() | |
280 | if self._current_group is not None: | |
281 | self._current_group.add_argument(*option_args, **option_kwargs) | |
282 | else: | |
283 | self.parser.add_argument(*option_args, **option_kwargs) | |
284 | self.options.append(option) | |
285 | if option.parse_from_config: | |
286 | name = option.config_name | |
287 | assert name is not None | |
288 | self.config_options_dict[name] = option | |
289 | self.config_options_dict[name.replace("_", "-")] = option | |
290 | LOG.debug('Registered option "%s".', option) | |
291 | ||
292 | def extend_default_ignore(self, error_codes: Sequence[str]) -> None: | |
293 | """Extend the default ignore list with the error codes provided. | |
294 | ||
295 | :param error_codes: | |
296 | List of strings that are the error/warning codes with which to | |
297 | extend the default ignore list. | |
298 | """ | |
299 | LOG.debug("Extending default ignore list with %r", error_codes) | |
300 | self.extended_default_ignore.extend(error_codes) | |
301 | ||
302 | def extend_default_select(self, error_codes: Sequence[str]) -> None: | |
303 | """Extend the default select list with the error codes provided. | |
304 | ||
305 | :param error_codes: | |
306 | List of strings that are the error/warning codes with which | |
307 | to extend the default select list. | |
308 | """ | |
309 | LOG.debug("Extending default select list with %r", error_codes) | |
310 | self.extended_default_select.extend(error_codes) | |
311 | ||
312 | def parse_args( | |
313 | self, | |
314 | args: Sequence[str] | None = None, | |
315 | values: argparse.Namespace | None = None, | |
316 | ) -> argparse.Namespace: | |
317 | """Proxy to calling the OptionParser's parse_args method.""" | |
318 | if values: | |
319 | self.parser.set_defaults(**vars(values)) | |
320 | return self.parser.parse_args(args) |