Coverage for /home/martinb/.local/share/virtualenvs/camcops/lib/python3.6/site-packages/_pytest/config/argparsing.py : 31%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1import argparse
2import sys
3import warnings
4from gettext import gettext
5from typing import Any
6from typing import Callable
7from typing import cast
8from typing import Dict
9from typing import List
10from typing import Mapping
11from typing import Optional
12from typing import Sequence
13from typing import Tuple
14from typing import Union
16import py
18import _pytest._io
19from _pytest.compat import TYPE_CHECKING
20from _pytest.config.exceptions import UsageError
22if TYPE_CHECKING:
23 from typing import NoReturn
24 from typing_extensions import Literal
26FILE_OR_DIR = "file_or_dir"
29class Parser:
30 """ Parser for command line arguments and ini-file values.
32 :ivar extra_info: dict of generic param -> value to display in case
33 there's an error processing the command line arguments.
34 """
36 prog = None # type: Optional[str]
38 def __init__(
39 self,
40 usage: Optional[str] = None,
41 processopt: Optional[Callable[["Argument"], None]] = None,
42 ) -> None:
43 self._anonymous = OptionGroup("custom options", parser=self)
44 self._groups = [] # type: List[OptionGroup]
45 self._processopt = processopt
46 self._usage = usage
47 self._inidict = {} # type: Dict[str, Tuple[str, Optional[str], Any]]
48 self._ininames = [] # type: List[str]
49 self.extra_info = {} # type: Dict[str, Any]
51 def processoption(self, option: "Argument") -> None:
52 if self._processopt:
53 if option.dest:
54 self._processopt(option)
56 def getgroup(
57 self, name: str, description: str = "", after: Optional[str] = None
58 ) -> "OptionGroup":
59 """ get (or create) a named option Group.
61 :name: name of the option group.
62 :description: long description for --help output.
63 :after: name of other group, used for ordering --help output.
65 The returned group object has an ``addoption`` method with the same
66 signature as :py:func:`parser.addoption
67 <_pytest.config.argparsing.Parser.addoption>` but will be shown in the
68 respective group in the output of ``pytest. --help``.
69 """
70 for group in self._groups:
71 if group.name == name:
72 return group
73 group = OptionGroup(name, description, parser=self)
74 i = 0
75 for i, grp in enumerate(self._groups):
76 if grp.name == after:
77 break
78 self._groups.insert(i + 1, group)
79 return group
81 def addoption(self, *opts: str, **attrs: Any) -> None:
82 """ register a command line option.
84 :opts: option names, can be short or long options.
85 :attrs: same attributes which the ``add_argument()`` function of the
86 `argparse library
87 <https://docs.python.org/library/argparse.html>`_
88 accepts.
90 After command line parsing options are available on the pytest config
91 object via ``config.option.NAME`` where ``NAME`` is usually set
92 by passing a ``dest`` attribute, for example
93 ``addoption("--long", dest="NAME", ...)``.
94 """
95 self._anonymous.addoption(*opts, **attrs)
97 def parse(
98 self,
99 args: Sequence[Union[str, py.path.local]],
100 namespace: Optional[argparse.Namespace] = None,
101 ) -> argparse.Namespace:
102 from _pytest._argcomplete import try_argcomplete
104 self.optparser = self._getparser()
105 try_argcomplete(self.optparser)
106 strargs = [str(x) if isinstance(x, py.path.local) else x for x in args]
107 return self.optparser.parse_args(strargs, namespace=namespace)
109 def _getparser(self) -> "MyOptionParser":
110 from _pytest._argcomplete import filescompleter
112 optparser = MyOptionParser(self, self.extra_info, prog=self.prog)
113 groups = self._groups + [self._anonymous]
114 for group in groups:
115 if group.options:
116 desc = group.description or group.name
117 arggroup = optparser.add_argument_group(desc)
118 for option in group.options:
119 n = option.names()
120 a = option.attrs()
121 arggroup.add_argument(*n, **a)
122 file_or_dir_arg = optparser.add_argument(FILE_OR_DIR, nargs="*")
123 # bash like autocompletion for dirs (appending '/')
124 # Type ignored because typeshed doesn't know about argcomplete.
125 file_or_dir_arg.completer = filescompleter # type: ignore
126 return optparser
128 def parse_setoption(
129 self,
130 args: Sequence[Union[str, py.path.local]],
131 option: argparse.Namespace,
132 namespace: Optional[argparse.Namespace] = None,
133 ) -> List[str]:
134 parsedoption = self.parse(args, namespace=namespace)
135 for name, value in parsedoption.__dict__.items():
136 setattr(option, name, value)
137 return cast(List[str], getattr(parsedoption, FILE_OR_DIR))
139 def parse_known_args(
140 self,
141 args: Sequence[Union[str, py.path.local]],
142 namespace: Optional[argparse.Namespace] = None,
143 ) -> argparse.Namespace:
144 """parses and returns a namespace object with known arguments at this
145 point.
146 """
147 return self.parse_known_and_unknown_args(args, namespace=namespace)[0]
149 def parse_known_and_unknown_args(
150 self,
151 args: Sequence[Union[str, py.path.local]],
152 namespace: Optional[argparse.Namespace] = None,
153 ) -> Tuple[argparse.Namespace, List[str]]:
154 """parses and returns a namespace object with known arguments, and
155 the remaining arguments unknown at this point.
156 """
157 optparser = self._getparser()
158 strargs = [str(x) if isinstance(x, py.path.local) else x for x in args]
159 return optparser.parse_known_args(strargs, namespace=namespace)
161 def addini(
162 self,
163 name: str,
164 help: str,
165 type: Optional["Literal['pathlist', 'args', 'linelist', 'bool']"] = None,
166 default=None,
167 ) -> None:
168 """ register an ini-file option.
170 :name: name of the ini-variable
171 :type: type of the variable, can be ``pathlist``, ``args``, ``linelist``
172 or ``bool``.
173 :default: default value if no ini-file option exists but is queried.
175 The value of ini-variables can be retrieved via a call to
176 :py:func:`config.getini(name) <_pytest.config.Config.getini>`.
177 """
178 assert type in (None, "pathlist", "args", "linelist", "bool")
179 self._inidict[name] = (help, type, default)
180 self._ininames.append(name)
183class ArgumentError(Exception):
184 """
185 Raised if an Argument instance is created with invalid or
186 inconsistent arguments.
187 """
189 def __init__(self, msg: str, option: Union["Argument", str]) -> None:
190 self.msg = msg
191 self.option_id = str(option)
193 def __str__(self) -> str:
194 if self.option_id:
195 return "option {}: {}".format(self.option_id, self.msg)
196 else:
197 return self.msg
200class Argument:
201 """class that mimics the necessary behaviour of optparse.Option
203 it's currently a least effort implementation
204 and ignoring choices and integer prefixes
205 https://docs.python.org/3/library/optparse.html#optparse-standard-option-types
206 """
208 _typ_map = {"int": int, "string": str, "float": float, "complex": complex}
210 def __init__(self, *names: str, **attrs: Any) -> None:
211 """store parms in private vars for use in add_argument"""
212 self._attrs = attrs
213 self._short_opts = [] # type: List[str]
214 self._long_opts = [] # type: List[str]
215 if "%default" in (attrs.get("help") or ""):
216 warnings.warn(
217 'pytest now uses argparse. "%default" should be'
218 ' changed to "%(default)s" ',
219 DeprecationWarning,
220 stacklevel=3,
221 )
222 try:
223 typ = attrs["type"]
224 except KeyError:
225 pass
226 else:
227 # this might raise a keyerror as well, don't want to catch that
228 if isinstance(typ, str):
229 if typ == "choice":
230 warnings.warn(
231 "`type` argument to addoption() is the string %r."
232 " For choices this is optional and can be omitted, "
233 " but when supplied should be a type (for example `str` or `int`)."
234 " (options: %s)" % (typ, names),
235 DeprecationWarning,
236 stacklevel=4,
237 )
238 # argparse expects a type here take it from
239 # the type of the first element
240 attrs["type"] = type(attrs["choices"][0])
241 else:
242 warnings.warn(
243 "`type` argument to addoption() is the string %r, "
244 " but when supplied should be a type (for example `str` or `int`)."
245 " (options: %s)" % (typ, names),
246 DeprecationWarning,
247 stacklevel=4,
248 )
249 attrs["type"] = Argument._typ_map[typ]
250 # used in test_parseopt -> test_parse_defaultgetter
251 self.type = attrs["type"]
252 else:
253 self.type = typ
254 try:
255 # attribute existence is tested in Config._processopt
256 self.default = attrs["default"]
257 except KeyError:
258 pass
259 self._set_opt_strings(names)
260 dest = attrs.get("dest") # type: Optional[str]
261 if dest:
262 self.dest = dest
263 elif self._long_opts:
264 self.dest = self._long_opts[0][2:].replace("-", "_")
265 else:
266 try:
267 self.dest = self._short_opts[0][1:]
268 except IndexError as e:
269 self.dest = "???" # Needed for the error repr.
270 raise ArgumentError("need a long or short option", self) from e
272 def names(self) -> List[str]:
273 return self._short_opts + self._long_opts
275 def attrs(self) -> Mapping[str, Any]:
276 # update any attributes set by processopt
277 attrs = "default dest help".split()
278 attrs.append(self.dest)
279 for attr in attrs:
280 try:
281 self._attrs[attr] = getattr(self, attr)
282 except AttributeError:
283 pass
284 if self._attrs.get("help"):
285 a = self._attrs["help"]
286 a = a.replace("%default", "%(default)s")
287 # a = a.replace('%prog', '%(prog)s')
288 self._attrs["help"] = a
289 return self._attrs
291 def _set_opt_strings(self, opts: Sequence[str]) -> None:
292 """directly from optparse
294 might not be necessary as this is passed to argparse later on"""
295 for opt in opts:
296 if len(opt) < 2:
297 raise ArgumentError(
298 "invalid option string %r: "
299 "must be at least two characters long" % opt,
300 self,
301 )
302 elif len(opt) == 2:
303 if not (opt[0] == "-" and opt[1] != "-"):
304 raise ArgumentError(
305 "invalid short option string %r: "
306 "must be of the form -x, (x any non-dash char)" % opt,
307 self,
308 )
309 self._short_opts.append(opt)
310 else:
311 if not (opt[0:2] == "--" and opt[2] != "-"):
312 raise ArgumentError(
313 "invalid long option string %r: "
314 "must start with --, followed by non-dash" % opt,
315 self,
316 )
317 self._long_opts.append(opt)
319 def __repr__(self) -> str:
320 args = [] # type: List[str]
321 if self._short_opts:
322 args += ["_short_opts: " + repr(self._short_opts)]
323 if self._long_opts:
324 args += ["_long_opts: " + repr(self._long_opts)]
325 args += ["dest: " + repr(self.dest)]
326 if hasattr(self, "type"):
327 args += ["type: " + repr(self.type)]
328 if hasattr(self, "default"):
329 args += ["default: " + repr(self.default)]
330 return "Argument({})".format(", ".join(args))
333class OptionGroup:
334 def __init__(
335 self, name: str, description: str = "", parser: Optional[Parser] = None
336 ) -> None:
337 self.name = name
338 self.description = description
339 self.options = [] # type: List[Argument]
340 self.parser = parser
342 def addoption(self, *optnames: str, **attrs: Any) -> None:
343 """ add an option to this group.
345 if a shortened version of a long option is specified it will
346 be suppressed in the help. addoption('--twowords', '--two-words')
347 results in help showing '--two-words' only, but --twowords gets
348 accepted **and** the automatic destination is in args.twowords
349 """
350 conflict = set(optnames).intersection(
351 name for opt in self.options for name in opt.names()
352 )
353 if conflict:
354 raise ValueError("option names %s already added" % conflict)
355 option = Argument(*optnames, **attrs)
356 self._addoption_instance(option, shortupper=False)
358 def _addoption(self, *optnames: str, **attrs: Any) -> None:
359 option = Argument(*optnames, **attrs)
360 self._addoption_instance(option, shortupper=True)
362 def _addoption_instance(self, option: "Argument", shortupper: bool = False) -> None:
363 if not shortupper:
364 for opt in option._short_opts:
365 if opt[0] == "-" and opt[1].islower():
366 raise ValueError("lowercase shortoptions reserved")
367 if self.parser:
368 self.parser.processoption(option)
369 self.options.append(option)
372class MyOptionParser(argparse.ArgumentParser):
373 def __init__(
374 self,
375 parser: Parser,
376 extra_info: Optional[Dict[str, Any]] = None,
377 prog: Optional[str] = None,
378 ) -> None:
379 self._parser = parser
380 argparse.ArgumentParser.__init__(
381 self,
382 prog=prog,
383 usage=parser._usage,
384 add_help=False,
385 formatter_class=DropShorterLongHelpFormatter,
386 allow_abbrev=False,
387 )
388 # extra_info is a dict of (param -> value) to display if there's
389 # an usage error to provide more contextual information to the user
390 self.extra_info = extra_info if extra_info else {}
392 def error(self, message: str) -> "NoReturn":
393 """Transform argparse error message into UsageError."""
394 msg = "{}: error: {}".format(self.prog, message)
396 if hasattr(self._parser, "_config_source_hint"):
397 # Type ignored because the attribute is set dynamically.
398 msg = "{} ({})".format(msg, self._parser._config_source_hint) # type: ignore
400 raise UsageError(self.format_usage() + msg)
402 # Type ignored because typeshed has a very complex type in the superclass.
403 def parse_args( # type: ignore
404 self,
405 args: Optional[Sequence[str]] = None,
406 namespace: Optional[argparse.Namespace] = None,
407 ) -> argparse.Namespace:
408 """allow splitting of positional arguments"""
409 parsed, unrecognized = self.parse_known_args(args, namespace)
410 if unrecognized:
411 for arg in unrecognized:
412 if arg and arg[0] == "-":
413 lines = ["unrecognized arguments: %s" % (" ".join(unrecognized))]
414 for k, v in sorted(self.extra_info.items()):
415 lines.append(" {}: {}".format(k, v))
416 self.error("\n".join(lines))
417 getattr(parsed, FILE_OR_DIR).extend(unrecognized)
418 return parsed
420 if sys.version_info[:2] < (3, 9): # pragma: no cover
421 # Backport of https://github.com/python/cpython/pull/14316 so we can
422 # disable long --argument abbreviations without breaking short flags.
423 def _parse_optional(
424 self, arg_string: str
425 ) -> Optional[Tuple[Optional[argparse.Action], str, Optional[str]]]:
426 if not arg_string:
427 return None
428 if not arg_string[0] in self.prefix_chars:
429 return None
430 if arg_string in self._option_string_actions:
431 action = self._option_string_actions[arg_string]
432 return action, arg_string, None
433 if len(arg_string) == 1:
434 return None
435 if "=" in arg_string:
436 option_string, explicit_arg = arg_string.split("=", 1)
437 if option_string in self._option_string_actions:
438 action = self._option_string_actions[option_string]
439 return action, option_string, explicit_arg
440 if self.allow_abbrev or not arg_string.startswith("--"):
441 option_tuples = self._get_option_tuples(arg_string)
442 if len(option_tuples) > 1:
443 msg = gettext(
444 "ambiguous option: %(option)s could match %(matches)s"
445 )
446 options = ", ".join(option for _, option, _ in option_tuples)
447 self.error(msg % {"option": arg_string, "matches": options})
448 elif len(option_tuples) == 1:
449 (option_tuple,) = option_tuples
450 return option_tuple
451 if self._negative_number_matcher.match(arg_string):
452 if not self._has_negative_number_optionals:
453 return None
454 if " " in arg_string:
455 return None
456 return None, arg_string, None
459class DropShorterLongHelpFormatter(argparse.HelpFormatter):
460 """shorten help for long options that differ only in extra hyphens
462 - collapse **long** options that are the same except for extra hyphens
463 - shortcut if there are only two options and one of them is a short one
464 - cache result on action object as this is called at least 2 times
465 """
467 def __init__(self, *args: Any, **kwargs: Any) -> None:
468 """Use more accurate terminal width via pylib."""
469 if "width" not in kwargs:
470 kwargs["width"] = _pytest._io.get_terminal_width()
471 super().__init__(*args, **kwargs)
473 def _format_action_invocation(self, action: argparse.Action) -> str:
474 orgstr = argparse.HelpFormatter._format_action_invocation(self, action)
475 if orgstr and orgstr[0] != "-": # only optional arguments
476 return orgstr
477 res = getattr(
478 action, "_formatted_action_invocation", None
479 ) # type: Optional[str]
480 if res:
481 return res
482 options = orgstr.split(", ")
483 if len(options) == 2 and (len(options[0]) == 2 or len(options[1]) == 2):
484 # a shortcut for '-h, --help' or '--abc', '-a'
485 action._formatted_action_invocation = orgstr # type: ignore
486 return orgstr
487 return_list = []
488 short_long = {} # type: Dict[str, str]
489 for option in options:
490 if len(option) == 2 or option[2] == " ":
491 continue
492 if not option.startswith("--"):
493 raise ArgumentError(
494 'long optional argument without "--": [%s]' % (option), option
495 )
496 xxoption = option[2:]
497 shortened = xxoption.replace("-", "")
498 if shortened not in short_long or len(short_long[shortened]) < len(
499 xxoption
500 ):
501 short_long[shortened] = xxoption
502 # now short_long has been filled out to the longest with dashes
503 # **and** we keep the right option ordering from add_argument
504 for option in options:
505 if len(option) == 2 or option[2] == " ":
506 return_list.append(option)
507 if option[2:] == short_long.get(option.replace("-", "")):
508 return_list.append(option.replace(" ", "=", 1))
509 formatted_action_invocation = ", ".join(return_list)
510 action._formatted_action_invocation = formatted_action_invocation # type: ignore
511 return formatted_action_invocation
513 def _split_lines(self, text, width):
514 """Wrap lines after splitting on original newlines.
516 This allows to have explicit line breaks in the help text.
517 """
518 import textwrap
520 lines = []
521 for line in text.splitlines():
522 lines.extend(textwrap.wrap(line.strip(), width))
523 return lines