1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
|
"""
Customize argparse logic for tox (also contains the base options).
"""
import argparse
import logging
import os
import sys
from argparse import SUPPRESS, Action, ArgumentDefaultsHelpFormatter, ArgumentParser, Namespace, _SubParsersAction
from itertools import chain
from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple, Type, TypeVar, cast
from tox.config.source.ini.convert import StrConvert
from tox.plugin import NAME
from tox.session.state import State
from .env_var import get_env_var
from .ini import IniConfig
if sys.version_info >= (3, 8):
from typing import Literal
else:
from typing_extensions import Literal # noqa
class ArgumentParserWithEnvAndConfig(ArgumentParser):
"""
Argument parser which updates its defaults by checking the configuration files and environmental variables.
"""
def __init__(self, *args: Any, **kwargs: Any) -> None:
self.file_config = IniConfig()
kwargs["epilog"] = self.file_config.epilog
super().__init__(*args, **kwargs)
def fix_defaults(self) -> None:
for action in self._actions:
self.fix_default(action)
def fix_default(self, action: Action) -> None:
if hasattr(action, "default") and hasattr(action, "dest") and action.default != SUPPRESS:
of_type = self.get_type(action)
key = action.dest
outcome = get_env_var(key, of_type=of_type)
if outcome is None and self.file_config:
outcome = self.file_config.get(key, of_type=of_type)
if outcome is not None:
action.default, default_value = outcome
action.default_source = default_value # type: ignore[attr-defined]
if isinstance(action, argparse._SubParsersAction): # noqa
for values in action.choices.values(): # noqa
if isinstance(values, ToxParser):
values.fix_defaults()
@staticmethod
def get_type(action: Action) -> Type[Any]:
of_type: Optional[Type[Any]] = getattr(action, "of_type", None)
if of_type is None:
if isinstance(action, argparse._AppendAction): # noqa
of_type = List[action.type] # type: ignore[name-defined]
elif isinstance(action, argparse._StoreAction) and action.choices: # noqa
loc = locals()
loc["Literal"] = Literal
as_literal = f"Literal[{', '.join(repr(i) for i in action.choices)}]"
of_type = eval(as_literal, globals(), loc)
elif action.default is not None:
of_type = type(action.default)
elif isinstance(action, argparse._StoreConstAction) and action.const is not None: # noqa
of_type = type(action.const)
else: # pragma: no cover
raise TypeError(action) # pragma: no cover
return of_type
class HelpFormatter(ArgumentDefaultsHelpFormatter):
"""
A help formatter that provides the default value and the source it comes from.
"""
def __init__(self, prog: str) -> None:
super().__init__(prog, max_help_position=42, width=240)
def _get_help_string(self, action: Action) -> Optional[str]:
text = super()._get_help_string(action) # noqa
if text is not None:
if hasattr(action, "default_source"):
default = " (default: %(default)s)"
if text.endswith(default):
text = f"{text[: -len(default)]} (default: %(default)s -> from %(default_source)s)"
return text
Handler = Callable[[State], Optional[int]]
ToxParserT = TypeVar("ToxParserT", bound="ToxParser")
class Parsed(Namespace):
@property
def verbosity(self) -> int:
result: int = max(self.verbose - self.quiet, 0)
return result
@property
def is_colored(self) -> bool:
return cast(bool, self.colored == "yes")
class ToxParser(ArgumentParserWithEnvAndConfig):
"""Argument parser for tox."""
def __init__(self, *args: Any, root: bool = False, add_cmd: bool = False, **kwargs: Any) -> None:
super().__init__(*args, **kwargs)
if root is True:
self._add_base_options()
self.handlers: Dict[str, Tuple[Any, Handler]] = {}
if add_cmd is True:
self._cmd: Optional[_SubParsersAction] = self.add_subparsers(
title="command", help="tox command to execute", dest="command"
)
self._cmd.required = False
self._cmd.default = "run"
else:
self._cmd = None
def add_command(self, cmd: str, aliases: Sequence[str], help_msg: str, handler: Handler) -> "ArgumentParser":
if self._cmd is None:
raise RuntimeError("no sub-command group allowed")
sub_parser = self._cmd.add_parser(cmd, help=help_msg, aliases=aliases, formatter_class=HelpFormatter)
content = sub_parser, handler
self.handlers[cmd] = content
for alias in aliases:
self.handlers[alias] = content
return cast(ToxParser, sub_parser)
def add_argument(self, *args: str, of_type: Optional[Type[Any]] = None, **kwargs: Any) -> Action:
result = super().add_argument(*args, **kwargs)
if of_type is not None:
result.of_type = of_type # type: ignore[attr-defined]
return result
@classmethod
def base(cls: Type[ToxParserT]) -> ToxParserT:
return cls(add_help=False, root=True)
@classmethod
def core(cls: Type[ToxParserT]) -> ToxParserT:
return cls(prog=NAME, formatter_class=HelpFormatter, add_cmd=True, root=True)
def _add_base_options(self) -> None:
"""Argument options that always make sense."""
from tox.report import LEVELS
level_map = "|".join("{} - {}".format(c, logging.getLevelName(l)) for c, l in sorted(list(LEVELS.items())))
verbosity_group = self.add_argument_group(
f"verbosity=verbose-quiet, default {logging.getLevelName(LEVELS[3])}, map {level_map}",
)
verbosity = verbosity_group.add_mutually_exclusive_group()
verbosity.add_argument("-v", "--verbose", action="count", dest="verbose", help="increase verbosity", default=2)
verbosity.add_argument("-q", "--quiet", action="count", dest="quiet", help="decrease verbosity", default=0)
converter = StrConvert()
if converter.to_bool(os.environ.get("NO_COLOR", "")):
color = "no"
elif converter.to_bool(os.environ.get("FORCE_COLOR", "")):
color = "yes"
else:
color = "yes" if sys.stdout.isatty() else "no"
verbosity_group.add_argument(
"--colored",
default=color,
choices=["yes", "no"],
help="should output be enriched with colors",
)
self.fix_defaults()
def parse(self, args: Sequence[str]) -> Tuple[Parsed, List[str]]:
args = self._inject_default_cmd(args)
result = Parsed()
_, unknown = super().parse_known_args(args, namespace=result)
return result, unknown
def _inject_default_cmd(self, args: Sequence[str]) -> Sequence[str]:
# if the users specifies no command we imply he wants run, however for this to work we need to inject it onto
# the argument parsers left side
if self._cmd is None: # no commands yet so must be all global, nothing to fix
return args
_global = {
k: v
for k, v in chain.from_iterable(
((j, isinstance(i, (argparse._StoreAction, argparse._AppendAction))) for j in i.option_strings) # noqa
for i in self._actions
if hasattr(i, "option_strings")
)
}
_global_single = {i[1:] for i in _global if len(i) == 2 and i.startswith("-")}
cmd_at = next((j for j, i in enumerate(args) if i in self._cmd.choices), None)
global_args: List[str] = []
command_args: List[str] = []
reorganize_to = cmd_at if cmd_at is not None else len(args)
at = 0
while at < reorganize_to:
arg = args[at]
needs_extra = False
is_global = False
if arg in _global:
needs_extra = _global[arg]
is_global = True
elif arg.startswith("-") and not (set(arg[1:]) - _global_single):
is_global = True
(global_args if is_global else command_args).append(arg)
at += 1
if needs_extra:
global_args.append(args[at])
at += 1
new_args = global_args
new_args.append(self._cmd.default if cmd_at is None else args[cmd_at])
new_args.extend(command_args)
new_args.extend(args[reorganize_to + 1 :])
return new_args
|